2014/04/11(金) 20:03:28_ .NET _ tb:0 _ Comments:2
.NET のチューニングとしてよく知られているテクニックとして
・文字の連結では、ある程度の大きさを持つ場合は、StringBuilder をなるべく使う
・Reflection は、遅いので、避ける。避けられないケースは、delegate をキャッシングしておく等が有効。
・値型の配列は、Boxing を避けるため ArrayList ではなく、Generic の配列か、System.Array を使う。
・.NET の例外処理は、いろいろ処理があり、遅いので、例外が発生しないようにコーディングすること。
がよく知られています。
私の個人的な経験で、有効だとおもわれたのは、(覚書程度ですが...)
・文字列を char で一文字ごとにチェックする場合、
private static void LookupMe1(string str)
{
for (int i = 0; i < str.Length; i++)
{
char c = s[i];
}
}
とコーディングするより、
public static void LookupMe2(string s)
{
char[] cs = s.ToCharArray();
int csLen = cs.Length;
for (int i = 0; i < csLen; i++)
{
char c = cs[i];
}
}
としたほうが 40% ほど高速です。ループごとにアクセスされる、文字列の Length プロパティのアクセスコストが意外に高くつきます。またstring.ToCharArray() しても内部の配列を取り出すので高速です。
(配列の"Count", "Length" プロパティは、文字列以外でも同様のことが言えます。配列がループ内で不変であれば、ループの前に数を変数にしてキャッシュして、毎回プロパティ・アクセスさせないようにしてください。)
・外部のライブラリとの連携などで、外部から、ファンクションを呼ばれるときに、渡されるデータ型が不明で、object で受けざるをえないケースが多々あります。
function void write(object param)
の場合は、内部で型を確認する必要が出てくる必要性がある場合がありますが、、
object ___val = null;
if(param is int)
{
___val = (int)param;
}else if(param is float)
{
___val = (float)param;
}else if(param is string)
{
___val = param as string;
}
というような "Is-As" 条件を繰り返してしまいがちで、その分命令数が増えてしまいます。
私は、これを "イズアズ地獄"と勝手に名付けたのですが、
複数回以上 "Is-As" を繰りかすのは、明らかに無駄で、遅くなる原因ですので、
public delegate object ObjectHandler(object node);
public static System.Collections.Generic.Dictionary<System.RuntimeTypeHandle, ObjectHandler> ObjectTypeSwither = createList();
public static Dictionary createList()
{
Dictionary list = new Dictionary()
list[typeof(DateTime).TypeHandle] = new ObjectHandler(dateTimeConverter);
list[typeof(System.String).TypeHandle] = new ObjectHandler(stringConverter);
list[typeof(double).TypeHandle] = new ObjectHandler(doubleConverter);
}
で、型毎のデリゲートリスト一覧を作成しておいて、
ObjectHandler handler;
if (ObjectHandleSwitcher.TryGetValue(unkownValue.GetType().TypeHandle, out handler))
{
return handler(unkownValue);
}else
{
// 上で補足できない型の処理を記述します。
}
とすれば、無駄な分岐がなくりますので、多くのケースで、高速化できます。
これは、下記のような例外処理の時でも、応用できます。
try
{
......
}
catch(Exception ex)
{
if(ex is ArgumentException)
{
ArgumentException auEx = (ArgumentException)ex;
....
}else if(ex is IndexOutOfRangeException)
{
IndexOutOfRangeException indexEx = (IndexOutOfRangeException)ex;
....
} else if(ex is OutOfMemoryException)
{
OutOfMemoryException outEx = (OutOfMemoryException)ex;
.....
}
}
例外の型で別の処理を細かく行いたい場合、↓のようにすれば、より読みやすくなります。
try
{
}catch(Exception ex)
{
ErrorHanlder exHandler = null;
if(exceptionSwither.TryGetValue(typeof(ex).TypeHandle, out exHandler))
{
return exHandler(ex);
}else
{
.....
}
}
・Char.IsWhiteSpace() は、ロケールにしたがった WhiteSpace で検証してくれるが、若干遅い。
対象が、Tab, CR, LF など空白文字が限定できるのであれば、自作のメソッドを作成すれば、高速化できる。
class Program
{
static void Main(string[] args)
{
char[] cArray = new char[] { '\0', 'a', 'b', 'd', 'E', '\t', '\r', '\n' };
DateTime dtStart = DateTime.Now;
for (int i = 0; i < 100000000; i++)
{
foreach (char c in cArray)
{
bool r = char.IsWhiteSpace(c);
}
}
TimeSpan tpSpan = DateTime.Now.Subtract(dtStart);
Console.WriteLine("Normal IsWhiteSpace() result : " + tpSpan.TotalMilliseconds.ToString() + " milliseconds");
dtStart = DateTime.Now;
for (int i = 0; i < 100000000; i++)
{
foreach (char c in cArray)
{
bool r = IsWhiteSpaceCustom(c);
}
}
tpSpan = DateTime.Now.Subtract(dtStart);
Console.WriteLine("Custom IsWhiteSpace() result : " + tpSpan.TotalMilliseconds.ToString() + " milliseconds");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsWhiteSpaceCustom(char c)
{
switch (c)
{
case (char)0:
case (char)9:
case (char)10:
case (char)11:
case (char)12:
case (char)13:
case (char)28:
case (char)29:
case (char)30:
case (char)31:
case (char)32:
case (char)133:
case (char)160:
case (char)8192:
case (char)8193:
case (char)8194:
case (char)8195:
case (char)8196:
case (char)8197:
case (char)8198:
case (char)8199:
case (char)8200:
case (char)8201:
case (char)8202:
case (char)8203:
case (char)8232: // line separator
case (char)8233: // paragraph separator
case (char)8239: // narrow no-break space
case (char)8287: // Separator, space
case (char)12288: // Separator, space (Zenkaku Space)
return true;
}
return false;
}
}
Normal IsWhiteSpace() result : 3471.3216 milliseconds
Custom IsWhiteSpace() result : 1977.3338 milliseconds
・Char.ToUpper(), Char.ToLower() は、本当に必要がないのであれば、呼ばないほうがよい。
(メソッドの呼び出しのコストを低減させるため)
char c = str[pos];
if(c >= 'A' && c <= 'Z')
{
c = toLowerFaster(c);
}
private static readonly char[] CharLowerList = new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
///
/// This methods 65% - 70 % faster than char.ToLower()
///
/// UpperCase Car (Only 'A'-'Z')
/// LowerCase Char
public static char FasterToLower(char c)
{
return CharLowerList[c - 65];
}
のほうが、早い。
・再帰メソッドは、.NET では、スタック操作に時間がかかり、スタック・オーバーフローしてしまうとランタイムの視点からすると致命的になりかねないため、可能であれば、while(),for...next などのループで置き換えたほうが良い。
・大量に生成され、破棄されることが前提のクラスを設計するときは、文字列のフィールドをフィールド宣言やコンストラクタで "" や String.Empty するのはなるべく避け、インスタンスを作成しないか、null を設定しておき、実際に文字列が利用されるときに、文字列インスタンスを設定して遅延させたほうがパフォーマンス的に良い。特に、文字列フィールが多い場合には、アプリケーションの全体的なパフォーマンスに大差が出る場合があります。
public class MyStringClass
{
public string str1 = null;
public string str2 = null;
public string str3 = null;
public MyStringClass()
{
// 空白文字は設定しません。
}
}
[検証]
50 の文字フィールドを持つクラスを用意して、
① なにも設定しない
② nulll をフィールドに設定
③"" の空白文字をそれぞれに設定した (private string s1 = "";)
①②③のクラスを100000000ループでインスタンスだけを生成する処理速度を比較
[結果]
①2294.1312 ms
②2290.131 ms
③18288.046 ms
①②はほぼ同じ結果ですが、③の空50 文字列のインスタンスを作成するほうが、おおよそ8倍の遅くなります。
・String.Index() は、StringComparer のオプションによって (Ordinal などを利用すれば)、OS API で検索するので、手書きの検索よりも早い。
・String.IndexOf() で一文字を検索するときは、char で検索したほうが早い。
・.NET 4.5 では、起動オプションに、マルチコア JIT のプロファイル API が追加されました。このオプションをオンにすると、WinForm でも、コアが複数あれば、「えっ」と驚くほど起動が早くなります。設定方法は、プロファイルの保管場所を指定することで、プロファイルが有効になります。
1度アプリケーションを起動すると、指定したパスにプロファイルされたファイルが作成され、2回目以降は、そのプロファイルされた情報とともに、高速な起動処理がおこなわれます。
(このオプションのためだけでも、開発プロジェクトを .NET 4.5 限定にする価値はあります。Good Job!)
ProfileOptimization.SetProfileRoot(string)
ProfileOptimization.StartProfile(string)
当たり前なのですが、Form.Load() で設定するのではなく(Form.load が呼ばれるのは起動完了したあとですからね...)、アセンブリのエントリ ポイント (Main) 直後におかないと効果は実感できないのでご注意を。
NGEN.exe は、厳密名をつけて、プリコンパイルして、GACに入れて...と設定が面倒なのですが、この API は手軽に導入できます。
SetProfileRoot を利用しはじめてから、特に必要がない限り以外は、NGEN.exe を使わないようになりました。
・URL の検証で、System.Uri は便利ですが、色々内部処理のためがインスタンスの生成が、かなり遅い。Protocol や hostname 等を取り出したいだけというのであれば、自作クラスに置き換えたほうが良い。(私のケースでは、10x - 30x ぐらい向上できました。)
・Random クラスは、万能ですが、インスタンスの生成を含め、少し遅い。ネットでさまざまな天才が、アルゴリズムを公開しているので、個人でベンチーマークしてみてください。一意の数値が欲しいだけであれば、System.Threading.Interlocked.Increment を有効活用してください。
・数値をパースするときは、.parse() よりも、tryparse() が断然早い。また、自作してみるのも、よいでしょう。
・.NET では、仕様上、プロパティが利用できるので、便利なのですが、プロパティアクセスのコストは少なくはありません。
プロパティは、set_Name(string s){} という感じでアセンブリにコンパイルされるメソッドの変形型なので、メソッドが呼ばれるコストとほぼ同じです。
(補足: 古いバージョンの .NET Framework だと差異が目立ちますが、最新のバージョン (4.0) ではかなり改善されてきています)
プロパティは、第三者に外部にクラスを公開するための手段だと考えたほうが良いかもしれません。アセンブリ内では、internal なフィールドでアクセスしたようが、体感的な速度が向上するケースが多いようです。(フィールドも、public でも、いいんじゃないかという場合もあるでしょう)。
私は、.NET の古いバージョンでも高速に動作させたいため、フィールド派(?)です。
・StringBuilder のデフォルトのCapacity は 3000 ですが、格納する文字列が平均して少ない場合、Capacity を少な目に設定したほうがパフォーマンス的に(ほんの僅かですが)良い。通常は気にしなくてもよい(笑
・性能アップのためには、
「推測で判断せず、検証を! 『Dont"t guess, test it!』...」
「ビルトインなメソッドが常に最速だと考えないこと 『Using .Net Method is not always the best answer!』...」
「検索するキーワードは"何とかメソッド" + ".NET" + "Performance"』...」 だけで、満足しないこと。
(Java と .NET の実装はライブラリには、メソッド名が同じか、偶然にもよく似ているケースが(笑)あるので、Java ユーザー の意見も、積極的に参考してください)
がポイントです。
(個人的な感想:チューニングや、デバッグは、とても時間のかかる困難な作業かもしれませんが、エンジニアとして最も成長できるのは、この期間です。);;
以上、あくまでも、個人的なメモ書きです。