blog

C#のパフォーマンス最適化の "ほんの一握り"

プログラムの読みやすさと性能は反比例する」という話をネットで聞いたことがあります。私はこの意見に大賛成なので、パフォーマンスの最適化の可読性に極端に影響される人のために、ここでは繰り返しませんが、今日...

Mar 23, 2025 · 24 min. read
シェア

長い時間の後に何かを書いていなかった、主に最近より忙しく、より主に最近より怠惰な......実際には、これは書くために非常に初期のようなものです。

しかし、CodeMonkeyを目指す者として、パフォーマンス最適化のヒントを紹介したいと思います。

私はかつてインターネット上でプログラムの読みやすさは、その性能に反比例するという格言を聞きました。私はこの言葉にとても同意するので、パフォーマンスの最適化の可読性に非常に影響を受ける人のために、ここでは繰り返しませんが、今日は主にパフォーマンスを最適化するために行うことができるいくつかの手についてです。

重複コードの削減

これは最も基本的な最適化スキームで、繰り返されることを最小限に抑え、一度しか行わないようにするものです。もっと一般的なのはこのコードで、同じMath.Cos(angle)とMath.Sin(angle)が2回行われます。

private Point RotatePt(double angle, Point pt)  
{  
     Point pRet = new Point();  
     angle = -angle;  
     pRet.X = (int)((double)pt.X * Math.Cos(angle) - (double)pt.Y * Math.Sin(angle));  
     pRet.Y = (int)((double)pt.X * Math.Sin(angle) + (double)pt.Y * Math.Cos(angle));  
     return pRet;  
} 

最適化後

private Point RotatePt3(double angle, Point pt)  
{  
    Point pRet = new Point();  
    angle = -angle;  
    double SIN_ANGLE = Math.Sin(angle);  
    double COS_ANGLE = Math.Cos(angle);  
    pRet.X =(int)(pt.X * COS_ANGLE - pt.Y * SIN_ANGLE);  
    pRet.Y = (int)(pt.X * SIN_ANGLE + pt.Y * COS_ANGLE);  
    return pRet;  
} 

メソッド内でオブジェクトをインスタンス化する別の方法もありますが、実際にはオブジェクトは再利用可能です。

public static string ConvertQuot(string html)  
{  
    Regex regex = new Regex("&(quot|#34);", RegexOptions.IgnoreCase);  
    return regex.Replace(html, """);  
} 

最適化後

readonly static Regex ReplaceQuot = new Regex("&(quot|#34);", RegexOptions.IgnoreCase | RegexOptions.Compiled);  
public static string ConvertQuot(string html)  
{  
    return ReplaceQuot.Replace(html, """);  
} 

また、初期化する必要のないoutパラメータを呼び出すなど、不必要な初期化もあります。

public bool Check(int userid)  
{  
    var user = new User();  
    if(GetUser(userid,out user))  
    {  
        return user.Level > 1;  
    }  
    return false;  
} 

ここでの new User() は不要な操作です。

最適化後

public bool Check(int userid)  
{  
    User user;  
    if(GetUser(userid,out user))  
    {  
        return user.Level > 1;  
    }  
    return false;  
} 

正規表現にこだわらない

偶然にも、○○○○の栗の中に表現対象であることも一緒に書かれていました。

多くの人は、正規表現は速い、とても速い、超高速だと思っています。

正規表現は非常に高速ですが、彼を信じてはいけない、あなたが次の栗を見て信じてはいけません。

//方法1  
public static string ConvertQuot1(string html)  
{  
    return html.Replace(""", """).Replace(""", """);  
}  
 
readonly static Regex ReplaceQuot = new Regex("&(quot|#34);", RegexOptions.IgnoreCase | RegexOptions.Compiled);  
//方法2  
public static string ConvertQuot2(string html)  
{  
    return ReplaceQuot.Replace(html, """);  
} 

正規表現の方が速いと思う人、手を挙げてください。

その結果、10Wサイクルの時間、たとえ10回連続のReplaceであっても、Regexより優れているので、彼を信じてはいけません。

//方法1  
public static string ConvertQuot1(string html)  
{  
    return html.Replace("0", "").Replace("1", "").Replace("2", "").Replace("3", "").Replace("4", "").Replace("5", "").Replace("6", "").Replace("7", "").Replace("8", "").Replace("9", "");  
}  
 
readonly static Regex ReplaceQuot = new Regex("", RegexOptions.IgnoreCase | RegexOptions.Compiled);  
//方法2  
public static string ConvertQuot2(string html)  
{  
    return ReplaceQuot.Replace(html, "");  
} 

コンバートクォート1:1538

ConvertQuot2:42179

*** 本物の、カップケーキの栗をお見せしましょう。

Htmlstring = Regex.Replace(Htmlstring, @"<(.[^>]*)>", "", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"([
])[\s]+", "", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"-->", "", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"<!--.*", "", RegexOptions.IgnoreCase);  
 
Htmlstring = Regex.Replace(Htmlstring, @"&(quot|#34);", """, RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(amp|#38);", "&", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(lt|#60);", "<", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(gt|#62);", ">", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(nbsp|#160);", " ", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(iexcl|#161);", "\xa1", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(cent|#162);", "\xa2", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(pound|#163);", "\xa3", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&(copy|#169);", "\xa9", RegexOptions.IgnoreCase);  
Htmlstring = Regex.Replace(Htmlstring, @"&#(\d+);", "", RegexOptions.IgnoreCase); 

#p#

正規表現の賢い使い方

上記の正規表現は効率的ではありませんが、彼を使用しないことを意味するものではありません、正規表現の役割は、少なくともそうではありません。

正規表現を使用する必要がある場合にも注意が必要で、できるだけ静的なグローバル公開にすることができます。

readonly static Regex regex = new Regex("", RegexOptions.Compiled); 

2番目のパラメータRegexOptions.Compiledは、正規表現をアセンブリにコンパイルすることを指定するアノテーションです。これにより、実行速度は速くなりますが、起動時間が長くなります。

一般論として、この列挙を追加すると、Regexオブジェクトの初期化が遅くなりますが、文字列検索の実行は速くなり、使用しないと、多くのクエリの初期化が遅くなります。

測定された差は非常に大きく、コードは比較されませんが、どのくらいの違いに興味を持って、自分で試すことができます。

列挙型もいくつかありますが、パフォーマンスに影響があるかどうかはわかりませんが、原則として使用したほうがいいでしょう。

  • RegexOptions.IgnoreCase // 大文字小文字を区別しないマッチングを指定します。
  • RegexOptions.Multiline // マルチラインモード。と $ ... の意味を変更します。 式の中に ^ と $ がない場合は
  • RegexOptions.Singleline // 単一行モードを指定します。変化点の意味 .... がない場合は、.がない場合は

コンパイラによる定数計算の前処理

コンパイラがプログラムのあるセクションをコンパイルするとき、ある演算が定数対定数であることがわかると、コンパイル時にその演算を行い、プログラムが実行されるときに演算を繰り返す必要がないようにします。

例えば

しかし、コンパイラーは時としてそれほど賢くはありません。

そんな時こそ、ちょっとした手助けが必要です。

括弧で囲んで、コンパイル時に演算ができるように、定数を最初に計算する必要があることを知らせます

文字列比較

ご存知の方も多いと思いますが、とりあえず書いておきます。

string s = "";  
1) if(s == ""){}  
2) if(s == string.Empty){}  
3) if (string.IsNullOrEmpty(s)) { }  
4) if(s != null && s.Length ==0) {}   
5) if((s+"").Length == 0){} 

1,2 最も遅い 3 速い 4,5 最も速い

1,2ほとんど差はありません 4,5ほとんど差はありませんが、これはnullと空の文字列の比較にのみ適用され、それが連続的な空白の場合はstring.IsNullOrWhiteSpaceが最速ですが、このメソッドは2.0内部ではありません。

つまり、2.0は次のようになります。

ここで重要なのは、s + "" という操作で null が "" に変換されることです。

2番目のパラメータには""かstring.Emptyしか指定できないので、蓄積にはほとんど時間がかかりません。

文字列の接続

文字列の蓄積は、この推論と正規表現、やみくもにStringBuilderを崇拝しないでください、文字列のスプライシングの多数では、StringBuilderは確かにStringBuilderを必要としない場合、固定文字列の蓄積の少数の役割をスピードアップの役割を果たすことができる、すべての後に、StringBuilderの初期化にも時間がかかります。StringBuilderの初期化にも時間がかかります。

ps: これを書いたのは覚えているのですが、投稿したらなぜか消えてしまいました・・・。

また、string.Concatメソッドがあり、メソッドは、プログラムの速度の小さな最適化することができます、大きさは非常に小さいです。

string.Joinとの違いは、スペーシング表記()がないことです。

よく遭遇するもう一つの文字列スプライス

public string JoinIds(List<User> users)  
{  
    StringBuilder sb = new StringBuilder();  
    foreach (var user in users)  
    {  
        sb.Append("'");  
        sb.Append(user.Id);  
        sb.Append("',");  
    }  
    sb.Length = sb.Length - 1;  
    return sb.ToString();  
} 

この場合、2つの最適なシナリオがあります。

3.5以上は直接Linqの補助を使用することができ、このプログラムのコードが少なくなりますが、パフォーマンスが相対的に悪いです

public string JoinIds(List<User> users)  
{  
    return "'" + string.Join("','", users.Select(it => it.Id)) + "'";  
} 

非3.5またはパフォーマンスが重要な場合

public string JoinIds(List<User> users)  
{  
    var ee = users.GetEnumerator();  
    StringBuilder sb = new StringBuilder();  
    if (ee.MoveNext())  
    {  
        sb.Append("'");  
        sb.Append(ee.Current.Id);  
        sb.Append("'");  
        while (ee.MoveNext())  
        {  
            sb.Append(",'");  
            sb.Append(ee.Current.Id);  
            sb.Append("'");  
        }  
    }  
    return sb.ToString();  
} 

#p#

ブール型の判定は

この現象は初心者プログラマーによく見られます。

//書き方1  
if(state == 1)  
{  
    return true;  
}  
else 
{  
    return false;  
}  
//書き方2  
return state == 1 ? true : false;  
//最適化  
return state == 1; 

タイプ判定

一般的な判定には2種類あります。

1、これはコードを書く方が良いに属していますが、パフォーマンスが比較的低い、理由はGetType()が多くの時間を消費することです。

Type type = obj.GetType();  
switch (type.Name)  
{  
    case "Int32":  
        break;  
    case "String":  
        break;  
    case "Boolean":  
        break;  
    case "DateTime":  
        break;  
    ...  
    ...  
    default:  
        break;  
} 

2、この属性はコードを書くのが面倒ですが、性能は非常に高いタイプです。

if (obj is string)  
{  
 
}  
else if (obj is int)  
{  
 
}  
else if (obj is DateTime)  
{  
 
}  
...  
...  
else 
{  
 
} 

実際、中間的な方法があります。

IConvertible conv = obj as IConvertible;  
if (conv != null)  
{  
    switch (conv.GetTypeCode())  
    {  
        case TypeCode.Boolean:  
            break;  
        case TypeCode.Byte:  
            break;  
        case TypeCode.Char:  
            break;  
        case TypeCode.DBNull:  
            break;  
        case TypeCode.DateTime:  
            break;  
        case TypeCode.Decimal:  
            break;  
        case TypeCode.Double:  
            break;  
        case TypeCode.Empty:  
            break;  
        case TypeCode.Int16:  
            break;  
        case TypeCode.Int32:  
            break;  
        ...  
        ...  
        default:  
            break;  
    }  
}  
else 
{  
    //その他の型を扱う  
} 

ほとんどの場合、これは機能しますが、IConvertibleを実装し、TypeCode.Int32を返す型がある場合、それは範囲外です。

インデックスとしての列挙の使用

以下は実際の例ですが、重要なポイントを強調するために、ソースコード内の4つ以上の冗長なブランチを削除するためにいくつかの変更を加えました。

enum TemplateCode  
{  
    None = 0,  
    Head = 1,  
    Menu = 2,  
    Foot = 3,  
    Welcome = 4,  
}  
 
public string GetHtml(TemplateCode tc)  
{  
    switch (tc)  
    {  
        case TemplateCode.Head:  
            return GetHead();  
        case TemplateCode.Menu:  
            return GetMenu();  
        case TemplateCode.Foot:  
            return GetFoot();  
        case TemplateCode.Welcome:  
            return GetWelcome();  
        default:  
            throw new ArgumentOutOfRangeException("tc");  
    }  
} 

最適化後

readonly static Func<string>[] GetTemplate = InitTemplateFunction();  
 
private static Func<string>[] InitTemplateFunction()  
{  
    var arr = new Func<string>[5];  
    arr[1] = GetHead;  
    arr[2] = GetMenu;  
    arr[3] = GetFoot;  
    arr[4] = GetWelcome;  
    return arr;  
}   
 
public string GetHtml(TemplateCode tc)  
{  
    var index = (int)tc;  
    if (index >= 1 && index <= 4)  
    {  
        return GetTemplate[index]();  
    }  
    throw new ArgumentOutOfRangeException("tc");  
} 

しかし、列挙が連続した数字とは限らない場合もあるので、Dictionaryを使うこともできます。

readonly static Dictionary<TemplateCode, Func<string>> TemplateDict = InitTemplateFunction();  
 
private static Dictionary<TemplateCode, Func<string>> InitTemplateFunction()  
{  
    var ditc = new Dictionary<TemplateCode, Func<string>>();  
    ditc.Add(TemplateCode.Head, GetHead);  
    ditc.Add(TemplateCode.Menu, GetMenu);  
    ditc.Add(TemplateCode.Foot, GetFoot);  
    ditc.Add(TemplateCode.Welcome, GetWelcome);  
    return ditc;  
}   
 
public string GetHtml(TemplateCode tc)  
{  
    Func<string> func;  
    if (TemplateDict.TryGetValue(tc,out func))  
    {  
        return func();  
    }  
    throw new ArgumentOutOfRangeException("tc");  
} 

この最適化は、分岐の数が多いときには効果的ですが、少ないときにはあまり役に立ちません。

#p#

文字タイプ Char、分岐判定処理テクニック

コンテンツのこの部分はより複雑であり、アプリケーションの範囲は限られている、あなたは通常、それを使用することはできません場合は無視することができます。

文字列オブジェクトを扱うとき、charの値を決定してからさらに演算を行う必要があることがあります。

public string Show(char c)  
{  
    if (c >= '0' && c <= '9')  
    {  
        return "数字";  
    }  
    else if (c >= 'a' && c <= 'z')  
    {  
        return "小文字";  
    }  
    else if (c >= 'A' && c <= 'Z')  
    {  
        return "大文字";  
    }  
    else if (c == '/' || c == '\\' || c == '|'  
        || c == '$' || c == '#' || c == '+' 
        || c == '%' || c == '&' || c == '-' 
        || c == '^' || c == '*' || c == '=')  
    {  
        return "特殊記号";  
    }  
    else if (c == ',' || c == '.' || c == '!' 
        || c == ':' || c == ';' || c == '?' 
        || c == '"' || c == '\'')  
    {  
        return "句読点 ";  
    }  
    else 
    {  
        return " ;  
    }  
} 

これはスペース・フォア・タイムの最適化ですが、スペース・フォア・タイムといっても、実際に無駄になるスペースはそれほど多くありません。

readonly static byte[] CharMap = InitCharMap();  
 
private static byte[] InitCharMap()  
{  
    var arr = new byte[char.MaxValue];  
    for (char i = '0'; i <= '9'; i++)  
    {  
        arr[i] = 1;  
    }  
    for (char i = 'a'; i <= 'z'; i++)  
    {  
        arr[i] = 2;  
    }  
    for (char i = 'A'; i <= 'Z'; i++)  
    {  
        arr[i] = 3;  
    }  
    arr['/'] = 4;  
    arr['\\'] = 4;  
    arr['|'] = 4;  
    arr['$'] = 4;  
    arr['#'] = 4;  
    arr['+'] = 4;  
    arr['%'] = 4;  
    arr['&'] = 4;  
    arr['-'] = 4;  
    arr['^'] = 4;  
    arr['*'] = 4;  
    arr['='] = 4;  
 
    arr[','] = 5;  
    arr['.'] = 5;  
    arr['!'] = 5;  
    arr[':'] = 5;  
    arr[';'] = 5;  
    arr['?'] = 5;  
    arr['"'] = 5;  
    arr['\''] = 5;  
    return arr;  
}  
 
public string Show(char c)  
{  
    switch (CharMap[c])  
    {  
        case 0:  
            return " ;  
        case 1:  
            return "数字";  
        case 2:  
            return "小文字";  
        case 3:  
            return "大文字";  
        case 4:  
            return "特殊記号";  
        case 5:  
            return "句読点 ";  
        default:  
            return " ;  
    }  
} 

本来は特殊記号の部分のみ12回の判定が必要ですが、修正後は1回の判定で結果が出ます。

/// <summary>  
/// <para> : が最初の文字になる。</para>  
/// <para> : 単語になる文字</para>  
/// <para> : 数字になる文字</para>  
/// <para> : 空白文字</para>  
/// <para>16を含む:エスケープ文字</para>  
/// <para></para>  
/// </summary>  
private readonly static byte[] _WordChars = new byte[char.MaxValue];  
private readonly static sbyte[] _UnicodeFlags = new sbyte;  
private readonly static sbyte[, ,] _DateTimeWords;  
static UnsafeJsonReader()  
{  
    for (int i = 0; i < 123; i++)  
    {  
        _UnicodeFlags[i] = -1;  
    }  
 
    _WordChars['-'] = 1 | 4;  
    _WordChars['+'] = 1 | 4;  
 
    _WordChars['$'] = 1 | 2;  
    _WordChars['_'] = 1 | 2;  
    for (char c = 'a'; c <= 'z'; c++)  
    {  
        _WordChars[c] = 1 | 2;  
        _UnicodeFlags[c] = (sbyte)(c - 'a' + 10);  
    }  
    for (char c = 'A'; c <= 'Z'; c++)  
    {  
        _WordChars[c] = 1 | 2;  
        _UnicodeFlags[c] = (sbyte)(c - 'A' + 10);  
    }  
 
    _WordChars['.'] = 1 | 2 | 4;  
    for (char c = '0'; c <= '9'; c++)  
    {  
        _WordChars[c] = 4;  
        _UnicodeFlags[c] = (sbyte)(c - '0');  
    }  
 
    //科学的表記法  
    _WordChars['e'] |= 4;  
    _WordChars['E'] |= 4;  
 
    _WordChars[' '] = 8;  
    _WordChars['\t'] = 8;  
    _WordChars['
'] = 8;  
    _WordChars['
'] = 8;  
 
 
    _WordChars['t'] |= 16;  
    _WordChars['r'] |= 16;  
    _WordChars['n'] |= 16;  
    _WordChars['f'] |= 16;  
    _WordChars['0'] |= 16;  
    _WordChars['"'] |= 16;  
    _WordChars['\''] |= 16;  
    _WordChars['\\'] |= 16;  
    _WordChars['/'] |= 16;  
 
 
    string[] a =  { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" };  
    string[] b =  { "mon", "tue", "wed", "thu", "fri", "sat", "sun" };  
    _DateTimeWords = new sbyte[23, 21, 25];  
 
    for (sbyte i = 0; i < a.Length; i++)  
    {  
        var d = a[i];  
        _DateTimeWords[d[0] - 97, d[1] - 97, d[2] - 97] = (sbyte)(i + 1);  
    }  
 
    for (sbyte i = 0; i < b.Length; i++)  
    {  
        var d = b[i];  
        _DateTimeWords[d[0] - 97, d[1] - 97, d[2] - 97] = (sbyte)-(i + 1);  
    }  
    _DateTimeWords['g' - 97, 'm' - 97, 't' - 97] = sbyte.MaxValue;  
}  
 
blqwより抜粋.Json  
Read next

CIMdata 2014 PLM Market and Industry Development Forumが成功裏に開催された。

第3回CIMdata 2014 PLM市場・産業発展フォーラム\ne-works創業者兼総経理の黄培氏が歓迎のスピーチを行い、本フォーラムに参加するゲスト、CIMdata社長のピーター・ビレロ氏、CIMdataリサーチディレクターのピーター・ビレロ氏を盛大に紹介しました。

Mar 23, 2025 · 4 min read