« .NET Micro FrameworkでSPIを使う | トップページ | Syntax Highlighter 3.0の使用方法 »

.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配下の組み込みメソッドを利用することが、高速化のポイントということになります。

« .NET Micro FrameworkでSPIを使う | トップページ | Syntax Highlighter 3.0の使用方法 »

dotNet MF」カテゴリの記事

コメント

 可読性の意味からも配列のコピーという目的の明らかな処理はArray.Copyを使うべきですね。私も.NET Compact FrameworkでネイティブのDLLをP/Invokeするような処理ではArray.Copyを使います。.NET FrameworkはMicro FrameworkでもBCLが結構、多くのものを実装してくれているのでこの辺をきちんと使える、BCLのメソッド群の所番地をはじき出せるようにするというのが、まあ手腕を図る目安なんじゃないかと。
 詳細はドキュメントを見るでも、存在していることを知らないと探しようもないですから。

コメントありがとうございました。
可読性の意味でも、ご指摘どおりArray.Copyを使うべきだと思います(Cだと、memcpy/strcpyといったライブラリもありますし)。NETMFクラスライブラリの全貌を押さえておく必要ありですね(Full NETに比べればはるかに小さいのだが)。

この記事へのコメントは終了しました。

« .NET Micro FrameworkでSPIを使う | トップページ | Syntax Highlighter 3.0の使用方法 »

2018年10月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ