.Net Micro Frameworkで配列を高速コピーする
前回の「NET Micro FrameworkでSPIを使う」にて作成したFEZ用SPI Flashメモリドライバ中に、byte配列をforループでまわしてコピー・結合するコードがあったのですが、FEZのフォーラムでArray.Copy()メソッドを使うべしという指摘をいただきました。試験的なコードを作って試してみると確かに劇的に早くなりました。
実行速度比較用のテストプログラム
以下のコードで、要素数500の配列のコピーを1000回繰り返す処理の実行時間を、
①CopyByLoop()の呼び出し → forループを使った処理
②System.Array.Copy()の呼び出し → NETMF組み込みのライブラリ呼び出し
の2パターンで比較します。
using System; using Microsoft.SPOT; namespace ArrayCopy { public class Program { public static void Main() { long begin, end, elapsed; int[] Array1 = new int[500]; int[] Array2 = new int[500]; const int N = 1000; int n; Debug.Print("Number of Elements:" + Array1.Length.ToString()); Debug.Print("Number of Loops :" + N.ToString()); InitArray(Array1, Array2); begin = DateTime.Now.Ticks; for (n = 0; n < N; n++) { CopyByLoop(Array1, Array2); } end = DateTime.Now.Ticks; elapsed = (end - begin) / TimeSpan.TicksPerMillisecond; Debug.Print("Time of CopyByLoop():" + elapsed.ToString()); InitArray(Array1, Array2); begin = DateTime.Now.Ticks; for (n = 0; n < N; n++) { Array.Copy(Array2, Array1, Array2.Length); } end = DateTime.Now.Ticks; elapsed = (end - begin) / TimeSpan.TicksPerMillisecond; Debug.Print("Time of Array.Copy():" + elapsed.ToString()); } static void InitArray(int[] Array1, int[] Array2) { for (int i = 0; i < Array1.Length; i++) { Array1[i] = 0; Array2[i] = i; } } static void CopyByLoop(int[] Array1, int[] Array2) { for (int i = 0; i < Array1.Length; i++) Array1[i] = Array2[i]; } } }
実行結果
以下Netduinoで実行した結果(単位はms)ですが、なんと、200倍近い性能差があります!
Number of Elements:500
Number of Loops :1000
Time of CopyByLoop():31120
Time of Array.Copy():164
結果の考察
極端な性能差が出る理由は、Array.Copy()はネイティブコードで動き、CopyByLoop()はforループをMSIL(中間コード)の逐次翻訳で動かしているためだと思われます。
ちなみに、PCで動作するFull NET Frameworkで同様のプログラムを動かすと、CopyByLoop(forループ)とArray.Copy()の呼び出しで殆ど差がつきません。この理由は、Full NET FrameworkははJITコンパイル方式のため、実行時にMSILをネイティブコードにコンパイルした結果がキャッシュされ2回目のループからはネイティブコードで動くためだと思われます。
Array.Copy()がネイティブコードで動くとすれば、NETMF PKのどこかにソースがある筈なので、そいつを探してみました。先ず、C#のコードがどのようなMSIL(中間コード)に翻訳されているのかをIldasm.exeを使って調べてみます。Ildasm.exeは.NET SDKに入っているツールで、.NET Framework の.exeファイルをを解析し、人間が読むことのできるMSILのコードを表示します。
以下にMain()関数を逆アセンブルした結果をを示します。
.method public hidebysig static void Main() cil managed { .entrypoint // コード サイズ 282 (0x11a) .maxstack 3 .locals init ([0] int64 begin, [1] int64 end, [2] int64 elapsed, [3] int32[] Array1, [4] int32[] Array2, [5] int32 n, [6] int32 CS$0$0000, [7] int32 CS$0$0001, [8] valuetype [mscorlib]System.DateTime CS$0$0002, [9] valuetype [mscorlib]System.DateTime CS$0$0003, [10] valuetype [mscorlib]System.DateTime CS$0$0004, [11] valuetype [mscorlib]System.DateTime CS$0$0005) IL_0000: ldc.i4 0x1f4 IL_0005: newarr [mscorlib]System.Int32 IL_000a: stloc.3 IL_000b: ldc.i4 0x1f4 IL_0010: newarr [mscorlib]System.Int32 IL_0015: stloc.s Array2 IL_0017: ldstr "Number of Elements:" IL_001c: ldloc.3 IL_001d: ldlen IL_001e: conv.i4 IL_001f: stloc.s CS$0$0000 IL_0021: ldloca.s CS$0$0000 IL_0023: call instance string [mscorlib]System.Int32::ToString() IL_0028: call string [mscorlib]System.String::Concat(string, string) IL_002d: call void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string) IL_0032: ldstr "Number of Loops :" IL_0037: ldc.i4 0x3e8 IL_003c: stloc.s CS$0$0001 IL_003e: ldloca.s CS$0$0001 IL_0040: call instance string [mscorlib]System.Int32::ToString() IL_0045: call string [mscorlib]System.String::Concat(string, string) IL_004a: call void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string) IL_004f: ldloc.3 IL_0050: ldloc.s Array2 IL_0052: call void ArrayCopy.Program::InitArray(int32[], int32[]) IL_0057: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now() IL_005c: stloc.s CS$0$0002 IL_005e: ldloca.s CS$0$0002 IL_0060: call instance int64 [mscorlib]System.DateTime::get_Ticks() IL_0065: stloc.0 IL_0066: ldc.i4.0 IL_0067: stloc.s n IL_0069: br.s IL_0079 IL_006b: ldloc.3 IL_006c: ldloc.s Array2 IL_006e: call void ArrayCopy.Program::CopyByLoop(int32[], int32[]) IL_0073: ldloc.s n IL_0075: ldc.i4.1 IL_0076: add IL_0077: stloc.s n IL_0079: ldloc.s n IL_007b: ldc.i4 0x3e8 IL_0080: blt.s IL_006b IL_0082: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now() IL_0087: stloc.s CS$0$0003 IL_0089: ldloca.s CS$0$0003 IL_008b: call instance int64 [mscorlib]System.DateTime::get_Ticks() IL_0090: stloc.1 IL_0091: ldloc.1 IL_0092: ldloc.0 IL_0093: sub IL_0094: ldc.i4 0x2710 IL_0099: conv.i8 IL_009a: div IL_009b: stloc.2 IL_009c: ldstr "Time of CopyByLoop():" IL_00a1: ldloca.s elapsed IL_00a3: call instance string [mscorlib]System.Int64::ToString() IL_00a8: call string [mscorlib]System.String::Concat(string, string) IL_00ad: call void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string) IL_00b2: ldloc.3 IL_00b3: ldloc.s Array2 IL_00b5: call void ArrayCopy.Program::InitArray(int32[], int32[]) IL_00ba: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now() IL_00bf: stloc.s CS$0$0004 IL_00c1: ldloca.s CS$0$0004 IL_00c3: call instance int64 [mscorlib]System.DateTime::get_Ticks() IL_00c8: stloc.0 IL_00c9: ldc.i4.0 IL_00ca: stloc.s n IL_00cc: br.s IL_00e0 IL_00ce: ldloc.s Array2 IL_00d0: ldloc.3 IL_00d1: ldloc.s Array2 IL_00d3: ldlen IL_00d4: conv.i4 IL_00d5: call void [mscorlib]System.Array::Copy(class [mscorlib]System.Array, class [mscorlib]System.Array, int32) IL_00da: ldloc.s n IL_00dc: ldc.i4.1 IL_00dd: add IL_00de: stloc.s n IL_00e0: ldloc.s n IL_00e2: ldc.i4 0x3e8 IL_00e7: blt.s IL_00ce IL_00e9: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now() IL_00ee: stloc.s CS$0$0005 IL_00f0: ldloca.s CS$0$0005 IL_00f2: call instance int64 [mscorlib]System.DateTime::get_Ticks() IL_00f7: stloc.1 IL_00f8: ldloc.1 IL_00f9: ldloc.0 IL_00fa: sub IL_00fb: ldc.i4 0x2710 IL_0100: conv.i8 IL_0101: div IL_0102: stloc.2 IL_0103: ldstr "Time of Array.Copy():" IL_0108: ldloca.s elapsed IL_010a: call instance string [mscorlib]System.Int64::ToString() IL_010f: call string [mscorlib]System.String::Concat(string, string) IL_0114: call void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string) IL_0119: ret } // end of method Program::Main
100行目の、
call void [mscorlib]System.Array::Copy(...)
が、System.Array.Copy()メソッドの呼び出し部分と思われます。
NETMF PKのソースコードから、Array::Copyでgrepすると、
MicroFrameworkPK_v4_1\CLR\Core\CLR_RT_HeapBlock_Array.cppの中に、
HRESULT CLR_RT_HeapBlock_Array::Copy( )という関数が存在し、こいつが呼び出されていると思われます。この関数内で、ヒープ内のメモリーコピーを行うことで配列のコピーを行っています。この関数はネイティブコードで動作するファームウェアの一部なので当然高速動作します。
ということでNETMFでは、配列の操作についてはSystem.Array配下の組み込みメソッドを利用することが、高速化のポイントということになります。
最近のコメント