none
Native C++とC#の呼び出しコストの差 RRS feed

  • 質問

  • C#とネイティブのメソッド呼び出しの速度差を図ってみました。
    C++コードです
    #include<windows.h>
    #include<stdio.h>
    const static int 数=1<<27;
    static int 値;
    class 速度{
    public:
    	static void 実行2(int a) {
    		値+=a;
    	}
    	static int 実行() {
    		int x=timeGetTime();
    		for(int a=0;a<数;a++) {
    			実行2(a);
    		}
    		return timeGetTime()-x;
    	}
    };
    int main(){
    	wchar_t buffer[1000];
    	swprintf(buffer,L"%dms,値=%d",速度::実行(),値);
    	::MessageBox(NULL,buffer,L"",MB_OK);
    	return 0;
    }
    

    C#コードです
    using System;
    using System.Security;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    class 速度 {
        public const int 数=1<<27;
        public static Int32 値=0;
        [SuppressUnmanagedCodeSecurity]
        [DllImport("winmm.dll")]
        static extern Int32 timeGetTime();
        [SuppressUnmanagedCodeSecurity]
        public static void 実行2(Int32 a) {
            値+=a;
        }
        [SuppressUnmanagedCodeSecurity]
        public static Int32 実行() {
            var s=timeGetTime();
            for(var a=0;a<数;a++) {
                実行2(a);
            }
            return timeGetTime()-s;
        }
    }
    static class M {
        static void Main() {
            MessageBox.Show(速度.実行().ToString()+"ms:値="+速度.値.ToString());
        }
    }
    

    それぞれReleaseで最適化ありでビルドし、デバッガではなく直接exeを実行することで速度を量りました。
    Native C++は156ms
    C#は260ms
    ぐらいです。
    演算に関しては同等の速度が出ているようですが、呼び出しがネイティブにかないません。
    オセロプログラムを作っていますが引数のカリー化にラムダ式を使うことでC++を超える速度を出せないかと考えています。
    しかし欠点として今回の呼び出しのオーバーヘッドが出てきました。
    StackクラスやMethodBase.GetCurrentMethod()などで現在のメソッドの情報を保持など何らかのオーバーヘッドがあるのが分かりますがどうにかしてこれをネイティブ並みに解決できないでしょうか?
    2009年8月31日 5:38

回答

  • さらに調査しました

    #pragma auto_inline(off)
    #pragma unmanaged
    int a;
    static void Native2回アクセス(){
    	a=0x7F;
    	a=0x8F;
    }
    static void Nativeループ(){
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    #pragma managed
    static void Managed2回アクセス(){
    	a=0x7F;
    	a=0x8F;
    }
    static void Managedループ(){
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    static void Managedループ先にアクセス(){
    	a=0x7F;//先立ってアクセス
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    void main(){
    	Native2回アクセス();
    	Managed2回アクセス();
    	Nativeループ();
    	Managedループ();
    	Managedループ先にアクセス();
    }
    

    メソッドに終わりにブレークポイントを置き、逆アセンブラで見てみました。

    static void Native2回アクセス(){
    00402320  push        ebp  
    00402321  mov         ebp,esp 
    	a=0x7F;
    00402323  mov         dword ptr [a (409394h)],7Fh 
    	a=0x8F;
    0040232D  mov         dword ptr [a (409394h)],8Fh 
    }
    00402337  pop         ebp  
    00402338  ret              
    
    static void Managed2回アクセス(){
    	a=0x7F;
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  cmp         dword ptr ds:[009B2E14h],0 
    0000000a  je          00000011 
    0000000c  call        79100E49 
    00000011  mov         dword ptr ds:[00409394h],7Fh 
    	a=0x8F;
    0000001b  mov         dword ptr ds:[00409394h],8Fh 
    }
    00000025  nop              
    00000026  pop         ebp  
    00000027  ret              
    
    ここから分かることは
    cmp dword ptr ds:[........],0
    je   ........
    call ........
    がstaticフィールドの初期化すべきかの処理だと思います。
    Managedメソッドではstaticフィールドが最初にアクセスするコードの直前にこの3つの命令が挿入されます。
    ループの場合、

    static void Nativeループ(){
    00402310  push        ebp  
    00402311  mov         ebp,esp 
    00402313  sub         esp,8 
    	for(int b=0;b<100;b++){
    00402316  mov         dword ptr [b],0 
    0040231D  jmp         `anonymous namespace'::Nativeループ+18h (402328h) 328h)
    0040231F  mov         eax,dword ptr [b] 
    00402322  add         eax,1 
    00402325  mov         dword ptr [b],eax 
    00402328  cmp         dword ptr [b],64h 
    0040232C  jge         `anonymous namespace'::Nativeループ+2Ah (40233Ah) 33Ah)
    		a=0x7F;
    0040232E  mov         dword ptr [a (40939Ch)],7Fh 
    	}
    00402338  jmp         `anonymous namespace'::Nativeループ+0Fh (40231Fh) 31Fh)
    	for(int b=0;b<100;b++){
    0040233A  mov         dword ptr [b],0 
    00402341  jmp         `anonymous namespace'::Nativeループ+3Ch (40234Ch) 34Ch)
    00402343  mov         ecx,dword ptr [b] 
    00402346  add         ecx,1 
    00402349  mov         dword ptr [b],ecx 
    0040234C  cmp         dword ptr [b],64h 
    00402350  jge         `anonymous namespace'::Nativeループ+4Eh (40235Eh) 35Eh)
    		a=0x7F;
    00402352  mov         dword ptr [a (40939Ch)],7Fh 
    0040235C  jmp         `anonymous namespace'::Nativeループ+33h (402343h) 343h)
    	}
    }
    0040235E  mov         esp,ebp 
    00402360  pop         ebp  
    00402361  ret              
    
    static void Managedループ(){
    	for(int b=0;b<100;b++){
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  sub         esp,8 
    00000006  cmp         dword ptr ds:[009B2E14h],0 
    0000000d  je          00000014 
    0000000f  call        79100E09 
    00000014  xor         edx,edx 
    00000016  mov         dword ptr [ebp-8],edx 
    00000019  xor         edx,edx 
    0000001b  mov         dword ptr [ebp-4],edx 
    0000001e  xor         edx,edx 
    00000020  mov         dword ptr [ebp-8],edx 
    00000023  nop              
    00000024  jmp         00000029 
    00000026  inc         dword ptr [ebp-8] 
    00000029  cmp         dword ptr [ebp-8],64h 
    0000002d  jge         0000003C 
    		a=0x7F;
    0000002f  mov         dword ptr ds:[0040939Ch],7Fh 
    	}
    00000039  nop              
    0000003a  jmp         00000026 
    	for(int b=0;b<100;b++){
    0000003c  xor         edx,edx 
    0000003e  mov         dword ptr [ebp-4],edx 
    00000041  nop              
    00000042  jmp         00000047 
    00000044  inc         dword ptr [ebp-4] 
    00000047  cmp         dword ptr [ebp-4],64h 
    0000004b  jge         0000005A 
    		a=0x7F;
    0000004d  mov         dword ptr ds:[0040939Ch],7Fh 
    00000057  nop              
    00000058  jmp         00000044 
    	}
    }
    0000005a  nop              
    0000005b  mov         esp,ebp 
    0000005d  pop         ebp  
    0000005e  ret              
    
    ループが複数合った場合、最初にアクセスされる部分に3命令挿入され大きなオーバーヘッドになります。
    これを防ぐためにループに先立ってアクセスしておくかローカル変数に代入することで解決できます。
    static void Managedループ先にアクセス(){
    	a=0x7F;//先立ってアクセス
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  push        eax  
    00000004  cmp         dword ptr ds:[009B2E14h],0 
    0000000b  je          00000012 
    0000000d  call        79100D81 
    00000012  xor         edx,edx 
    00000014  mov         dword ptr [ebp-4],edx 
    00000017  mov         dword ptr ds:[0040939Ch],7Fh 
    	for(int b=0;b<100;b++){
    00000021  xor         edx,edx 
    00000023  mov         dword ptr [ebp-4],edx 
    00000026  nop              
    00000027  jmp         0000002C 
    00000029  inc         dword ptr [ebp-4] 
    0000002c  cmp         dword ptr [ebp-4],64h 
    00000030  jge         0000003F 
    		a=0x7F;
    00000032  mov         dword ptr ds:[0040939Ch],7Fh 
    0000003c  nop              
    0000003d  jmp         00000029 
    	}
    }
    0000003f  nop              
    00000040  mov         esp,ebp 
    00000042  pop         ebp  
    00000043  ret              
    
    結論としてstaticフィールドはプログラム全体で共有するオブジェクトですが、小さな関数でアクセスすると余計なオーバヘッドが発生します。
    引数やローカル変数にしてもメソッド内でアクセスする回数が少ないなら問題は解決しません。
    多くのアクセスを1メソッド内で完結するとパフォーマンス低下を防ぐことが出来ます。

     

     

    • 回答としてマーク 和和和 2010年4月13日 3:48
    2010年4月13日 3:47

すべての返信

  • このコードのC++で and JIT コンパイルされた IA86 アセンブリコードは出せますか?

    JIT コンパイルされた IA86 アセンブリコードは Debugger.Break()を時間計測開始前に置いてリリースモードで実行し、デバッガに読み込ませて逆アセンブラ表示を出します。
    おそらくですがメソッドの呼び出しより、 繰り返しで使用している for に速度差の原因がありそうな気がします。

    (もしくはC#側がx64実行されてるとか?)


    Kazuhiko Kikuchi
    2009年8月31日 6:01
  • C# 2008Express,C++2008Expressを使っています。
    逆アセンブラはC++2008Expressしか使えないので、ネイティブC++とマネージドC++で作り直しました。
    デバッグできるようにデバッグモードでビルドしました。
    マネージドC++混合モードです
        static Int64 実行() {
    00000000  push        ebp 
    00000001  mov         ebp,esp
    00000003  sub         esp,1Ch
    00000006  cmp         dword ptr ds:[009B2E14h],0
    0000000d  je          00000014
    0000000f  call        790FDFC9
    00000014  xor         edx,edx
    00000016  mov         dword ptr [ebp-18h],edx
    00000019  xor         edx,edx
    0000001b  mov         dword ptr [ebp-4],edx
    0000001e  mov         dword ptr [ebp-0Ch],0
    00000025  mov         dword ptr [ebp-8],0
    0000002c  xor         edx,edx
    0000002e  mov         dword ptr [ebp-18h],edx
      Stopwatch ^s=Stopwatch::StartNew();
    00000031  call        79A242B0
    00000036  mov         dword ptr [ebp-1Ch],eax
    00000039  mov         eax,dword ptr [ebp-1Ch]
    0000003c  mov         dword ptr [ebp-18h],eax
            for(int a=0;a<数;a++) {
    0000003f  xor         edx,edx
    00000041  mov         dword ptr [ebp-4],edx
    00000044  nop             
    00000045  jmp         0000004A
    00000047  inc         dword ptr [ebp-4]
    0000004a  cmp         dword ptr [ebp-4],8000000h
    00000051  jge         0000005F
                実行2(a);
    00000053  mov         ecx,dword ptr [ebp-4]
    00000056  call        dword ptr ds:[009B35F0h]
            }
    0000005c  nop             
    0000005d  jmp         00000047
      s->Stop();
    0000005f  mov         ecx,dword ptr [ebp-18h]
    00000062  call        79A24214
            return s->ElapsedMilliseconds;
    00000067  mov         ecx,dword ptr [ebp-18h]
    0000006a  call        795CEE40
    0000006f  mov         dword ptr [ebp-14h],eax
    00000072  mov         dword ptr [ebp-10h],edx
    00000075  mov         eax,dword ptr [ebp-14h]
    00000078  mov         edx,dword ptr [ebp-10h]
    0000007b  mov         dword ptr [ebp-0Ch],eax
    0000007e  mov         dword ptr [ebp-8],edx
        }

    ネイティブC++混合モードです

    static int 実行() {
    00401130  push        ebp  
    00401131  mov         ebp,esp 
    00401133  sub         esp,0D8h 
    00401139  push        ebx  
    0040113A  push        esi  
    0040113B  push        edi  
    0040113C  lea         edi,[ebp-0D8h] 
    00401142  mov         ecx,36h 
    00401147  mov         eax,0CCCCCCCCh 
    0040114C  rep stos    dword ptr es:[edi] 
    		int x=timeGetTime();
    0040114E  mov         esi,esp 
    00401150  call        dword ptr [__imp__timeGetTime@0 (4030F0h)] 
    00401156  cmp         esi,esp 
    00401158  call        _RTC_CheckEsp (401200h) 
    0040115D  mov         dword ptr [x],eax 
    		for(int a=0;a<数;a++) {
    00401160  mov         dword ptr [a],0 
    00401167  jmp         速度::実行+42h (401172h) 01172h)
    00401169  mov         eax,dword ptr [a] 
    0040116C  add         eax,1 
    0040116F  mov         dword ptr [a],eax 
    00401172  cmp         dword ptr [a],8000000h 
    00401179  jge         速度::実行+59h (401189h) 01189h)
    			実行2(a);
    0040117B  mov         eax,dword ptr [a] 
    0040117E  push        eax  
    0040117F  call        速度::実行2 (4011B0h) 011B0h)
    00401184  add         esp,4 
    		}
    00401187  jmp         速度::実行+39h (401169h) 01169h)
    		return timeGetTime()-x;
    00401189  mov         esi,esp 
    0040118B  call        dword ptr [__imp__timeGetTime@0 (4030F0h)] 
    00401191  cmp         esi,esp 
    00401193  call        _RTC_CheckEsp (401200h) 
    00401198  sub         eax,dword ptr [x] 
    	}
    

    ネイティブC++ソースです

    #include<windows.h>
    #include<stdio.h>
    const static int 数=1<<27;
    static int 値;
    class 速度{
    public:
    	static void 実行2(int a) {
    		値+=a;
    	}
    	static int 実行() {
    		int x=timeGetTime();
    		for(int a=0;a<数;a++) {
    			実行2(a);
    		}
    		return timeGetTime()-x;
    	}
    };
    int main(){
    	wchar_t buffer[1000];
    	swprintf(buffer,L"%dms,値=%d",速度::実行(),値);
    	::MessageBox(NULL,buffer,L"",MB_OK);
    	return 0;
    }
    
    

    マネージドC++ソースです

    #pragma once
    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;
    using namespace System::Diagnostics;
    using namespace System::Runtime::InteropServices;
    using namespace System::Security;
    const int 数=1<<27;
    static Int32 値=0;
    class 速度 {
    public:
        [SuppressUnmanagedCodeSecurity]
        static void 実行2(Int32 a) {
            値+=a;
        }
        [SuppressUnmanagedCodeSecurity]
        static Int64 実行() {
    		Stopwatch ^s=Stopwatch::StartNew();
            for(int a=0;a<数;a++) {
                実行2(a);
            }
    		s->Stop();
            return s->ElapsedMilliseconds;
        }
    };
    void main(){
    	 MessageBox::Show(速度::実行().ToString()+"ms:値="+値.ToString());
    }
    

    確かにアセンブリコードはforループでも異なるようです。

    2009年8月31日 8:13
  • 何をしているか意味わかってます?

    >デバッグできるようにデバッグモードでビルドしました。

     ビルドモードを変えて実行コードが変わってしまって性能差の原因を調べられると思います?
     重箱の隅的に何が遅いとか早いとかやるのはそのあたりの根本的な事の理解をしてからにした方が良いですよ。


    Kazuhiko Kikuchi
    2009年8月31日 9:23
  • 関数呼び出しのオーバーヘッドもなにも、ネイティブ側のstatic void 実行2(int a)」ってインライン展開対象だし。
    JITだってインライン展開するだろうし。

    ネイティブ側ループ(/O2のみ指定。VC++2008SP1, x86。/Fa結果)
    $LL3@:
    	add	ecx, eax
    	inc	eax
    	cmp	eax, 134217728				; 08000000H
    	jl	SHORT $LL3@
    	mov	DWORD PTR _値, ecx
    
    C#側ループ(JIT後。NET3.5SP1,x86。Debugger.Break()仕込んでデバッガアタッチ。逆アセンブルリストへ)
    0000006a  add         dword ptr ds:[00A92FE8h],eax 
    00000070  add         eax,1 
    00000073  cmp         eax,8000000h 
    00000078  jl          0000006A 
    結局インクリメントする変数がレジスタに載ってるか、メモリに載ってるかの違いしかないし。

    jzkey
    2009年8月31日 13:04
  • C++の静的メンバ変数は最後の1回しか書き込まれていませんが、C#の静的クラスフィールドは毎回更新されていますね。
    と思ったら、C++側は大域変数でした。

    速度を稼ぐにはフィールドの値を変数に保存して、フィールドへのアクセス頻度を下げることですね。
    2009年8月31日 13:14
  • 何をしているか意味わかってます?

    >デバッグできるようにデバッグモードでビルドしました。

    ビルドモードを変えて実行コードが変わってしまって性能差の原因を調べられると思います?
    重箱の隅的に何が遅いとか早いとかやるのはそのあたりの根本的な事の理解をしてからにした方が良いですよ。


    Kazuhiko Kikuchi

    ブレークポイントで止めれないのでやむを得ませんでした。
    しかし両方とも同じ条件であれば性能は逆転するようなコードではないだろうという希望的観測からそうしました。

    と、思ってましたがブレークポイントで止めれないからDebugger.Break()を出されたのに今気づきました。
    2009年8月31日 14:48
  • 検証ありがとうございました。
    変数がレジスタ上かメモリ上であるか、というのはネイティブコンパイラとJITの最適化の違いってことらしいですね。
    そうなるとインライン展開されないようにした空関数をどうにかして作ってそれで比較するしかないようですね。
    2009年8月31日 14:59
  • C++の静的メンバ変数は最後の1回しか書き込まれていませんが、C#の静的クラスフィールドは毎回更新されていますね。
    と思ったら、C++側は大域変数でした。

    速度を稼ぐにはフィールドの値を変数に保存して、フィールドへのアクセス頻度を下げることですね。

    this参照ではないのでそのオーバーヘッドはありませんがこれもまたネイティブコンパイラとJITのグローバル(static)変数の最適化の違いみたいですね。
    2009年8月31日 15:20
  • 最適化の違いよりはスコープの違いかと。
    .NETでは最悪メタデータからフィールドを参照することができます。C++の場合、例えばリンク時コード生成中に外部からのアクセスがないことが確定すれば、多少ルーズなアクセス方法をとることもできるかと。
    # まぁ、どちらも可能性の話なので、実際に双方のコンパイラがどういう基準で動いているのかはわかりません。
    2009年9月2日 3:53
  • 時間がたちましたが再び計測する機会がありましたので結果を報告します。

    C++

    #include<windows.h>
    #include<stdio.h>
    const static int 数=1<<29;
    static int 値;
    #pragma auto_inline(off)
    static void 実行2(int a) {
    	値+=a;
    }
    int __stdcall WinMain(HINSTANCE hInstance, 
    	HINSTANCE hPrevInstance, 
    	LPSTR lpCmdLine, 
    	int nCmdShow
    ){
    	wchar_t buffer[1000];
    	int s=timeGetTime();
    	for(int a=0;a<数;a++)実行2(a);
    	swprintf(buffer,L"%dms,値=%d",timeGetTime()-s,値);
    	::MessageBox(NULL,buffer,L"",MB_OK);
    	return 0;
    }
    
    

    C#

    using System;
    using System.Security;
    using System.Windows.Forms;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    class 速度 {
        const int 数=1<<29;
        static Int32 値=0;
        [SuppressUnmanagedCodeSecurity]
        [DllImport("winmm")]
        static extern Int32 timeGetTime();
        [SuppressUnmanagedCodeSecurity]
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void 実行2(Int32 a) {
            値+=a;
        }
        static void Main() {
            var s=timeGetTime();
            for(var a=0;a<数;a++)実行2(a);
            MessageBox.Show((timeGetTime()-s).ToString()+"ms:値="+値.ToString());
        }
    }
    

    インライン化しない場合、
    C++:2203ms
    C#:2828ms

    インライン化した場合、
    #pragma auto_inline(on)
    //[MethodImpl(MethodImplOptions.NoInlining)]
    C++:812ms
    C#:2140ms

    C#の方が呼び出しコスト、ループ処理自体の処理ともに多いようです。
    原因はなにか・・C#Expressしかもっていませんがそれ以外のエディションは逆アセンブラがあるようですので機会があればまた調べます。

    2010年4月7日 3:30
  •  おそらく佐祐理さんの意見が正しいという結果がでました。
    先ほどの検証プログラムのグローバル変数をローカル変数を参照渡ししてみます。

    C++

    #include<windows.h>
    #include<stdio.h>
    const static int 数=1<<29;
    #pragma auto_inline(off)
    static void 実行2(int a,int &値) {
    	値+=a;
    }
    int __stdcall WinMain(HINSTANCE hInstance, 
    	HINSTANCE hPrevInstance, 
    	LPSTR lpCmdLine, 
    	int nCmdShow
    ){
    	wchar_t buffer[1000];
    	int s=timeGetTime();
    	int 値=0;
    	for(int a=0;a<数;a++)実行2(a,値);
    	swprintf(buffer,L"%dms,値=%d",timeGetTime()-s,値);
    	::MessageBox(NULL,buffer,L"",MB_OK);
    	return 0;
    }

    C#

    using System;
    using System.Security;
    using System.Windows.Forms;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    class 速度 {
        const int 数=1<<29;
        [SuppressUnmanagedCodeSecurity]
        [DllImport("winmm")]
        static extern int timeGetTime();
        [SuppressUnmanagedCodeSecurity]
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void 実行2(int a,ref int 値) {
            値+=a;
        }
        static void Main() {
            int 値=0;
            int s=timeGetTime();
            for(int a=0;a<数;a++) 実行2(a,ref 値);
            MessageBox.Show((timeGetTime()-s).ToString()+"ms:値="+値.ToString());
        }
    }
    

    C++:2594ms
    C#:2578ms

    結論と言い切っていいか分かりませんが.NETではグローバル変数の最適化がネイティブC++ほどはやってくれないということですね。
    心あたりがあるとすればあるstaticクラスのstaticメンバの初期化がそのクラスにアクセス(Reflectionでも)したときに発生するという現象があるので、初期化されているか、アクセスがあったか、と言った情報を毎回参照しているのかもしれません。 

    2010年4月7日 3:40
  • static変数にアクセスするときどういうアセンブラに変換されるか調べました。
    VC++2008Expressは逆アセンブラが付いているのでこでマネージドとネイティブコードを比較しました。
    プログラムは両方とも同じ以下のコードです。

    int a;
    void main(){
    	a=0x7F;
    }
    

    ネイティブアセンブラ

    int a;
    void main(){
    	a=0x7F;
    00401000  mov         dword ptr [a (403370h)],7Fh 
    }
    0040100A  xor         eax,eax 
    0040100C  ret    
    

    マネージドアセンブラ

    int a;
    void main(){
    	a=0x7F;
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  cmp         dword ptr ds:[009B31B0h],0 
    0000000a  je          00000011 
    0000000c  call        78F80E89 
    00000011  mov         dword ptr ds:[00409388h],7Fh 
    }
    0000001b  xor         eax,eax 
    0000001d  pop         ebp  
    0000001e  ret        
    
    関数の終了時の後処理が違うのもありますが、書き込みはアクセス直前に5つの命令が挿入されておりこれが遅さの原因だと思われます。
    属性などでこの処理を省けたらいいのですが。


    2010年4月8日 5:46
  • 1つのプロジェクトでもう少し簡単に検証できるようにしました。

    マネージドC++プロジェクトにて、

    #pragma unmanaged
    int a;
    static void Native速度(){
    	a=0x7F;
    }
    #pragma managed
    static void Managed速度(){
    	a=0x7F;
    }
    void main(){
    	Native速度();
    	Managed速度();
    }
    

    とすると実際の命令の比較が直ぐに分かります。


    2010年4月12日 7:36
  • さらに調査しました

    #pragma auto_inline(off)
    #pragma unmanaged
    int a;
    static void Native2回アクセス(){
    	a=0x7F;
    	a=0x8F;
    }
    static void Nativeループ(){
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    #pragma managed
    static void Managed2回アクセス(){
    	a=0x7F;
    	a=0x8F;
    }
    static void Managedループ(){
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    static void Managedループ先にアクセス(){
    	a=0x7F;//先立ってアクセス
    	for(int b=0;b<100;b++){
    		a=0x7F;
    	}
    }
    void main(){
    	Native2回アクセス();
    	Managed2回アクセス();
    	Nativeループ();
    	Managedループ();
    	Managedループ先にアクセス();
    }
    

    メソッドに終わりにブレークポイントを置き、逆アセンブラで見てみました。

    static void Native2回アクセス(){
    00402320  push        ebp  
    00402321  mov         ebp,esp 
    	a=0x7F;
    00402323  mov         dword ptr [a (409394h)],7Fh 
    	a=0x8F;
    0040232D  mov         dword ptr [a (409394h)],8Fh 
    }
    00402337  pop         ebp  
    00402338  ret              
    
    static void Managed2回アクセス(){
    	a=0x7F;
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  cmp         dword ptr ds:[009B2E14h],0 
    0000000a  je          00000011 
    0000000c  call        79100E49 
    00000011  mov         dword ptr ds:[00409394h],7Fh 
    	a=0x8F;
    0000001b  mov         dword ptr ds:[00409394h],8Fh 
    }
    00000025  nop              
    00000026  pop         ebp  
    00000027  ret              
    
    ここから分かることは
    cmp dword ptr ds:[........],0
    je   ........
    call ........
    がstaticフィールドの初期化すべきかの処理だと思います。
    Managedメソッドではstaticフィールドが最初にアクセスするコードの直前にこの3つの命令が挿入されます。
    ループの場合、

    static void Nativeループ(){
    00402310  push        ebp  
    00402311  mov         ebp,esp 
    00402313  sub         esp,8 
    	for(int b=0;b<100;b++){
    00402316  mov         dword ptr [b],0 
    0040231D  jmp         `anonymous namespace'::Nativeループ+18h (402328h) 328h)
    0040231F  mov         eax,dword ptr [b] 
    00402322  add         eax,1 
    00402325  mov         dword ptr [b],eax 
    00402328  cmp         dword ptr [b],64h 
    0040232C  jge         `anonymous namespace'::Nativeループ+2Ah (40233Ah) 33Ah)
    		a=0x7F;
    0040232E  mov         dword ptr [a (40939Ch)],7Fh 
    	}
    00402338  jmp         `anonymous namespace'::Nativeループ+0Fh (40231Fh) 31Fh)
    	for(int b=0;b<100;b++){
    0040233A  mov         dword ptr [b],0 
    00402341  jmp         `anonymous namespace'::Nativeループ+3Ch (40234Ch) 34Ch)
    00402343  mov         ecx,dword ptr [b] 
    00402346  add         ecx,1 
    00402349  mov         dword ptr [b],ecx 
    0040234C  cmp         dword ptr [b],64h 
    00402350  jge         `anonymous namespace'::Nativeループ+4Eh (40235Eh) 35Eh)
    		a=0x7F;
    00402352  mov         dword ptr [a (40939Ch)],7Fh 
    0040235C  jmp         `anonymous namespace'::Nativeループ+33h (402343h) 343h)
    	}
    }
    0040235E  mov         esp,ebp 
    00402360  pop         ebp  
    00402361  ret              
    
    static void Managedループ(){
    	for(int b=0;b<100;b++){
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  sub         esp,8 
    00000006  cmp         dword ptr ds:[009B2E14h],0 
    0000000d  je          00000014 
    0000000f  call        79100E09 
    00000014  xor         edx,edx 
    00000016  mov         dword ptr [ebp-8],edx 
    00000019  xor         edx,edx 
    0000001b  mov         dword ptr [ebp-4],edx 
    0000001e  xor         edx,edx 
    00000020  mov         dword ptr [ebp-8],edx 
    00000023  nop              
    00000024  jmp         00000029 
    00000026  inc         dword ptr [ebp-8] 
    00000029  cmp         dword ptr [ebp-8],64h 
    0000002d  jge         0000003C 
    		a=0x7F;
    0000002f  mov         dword ptr ds:[0040939Ch],7Fh 
    	}
    00000039  nop              
    0000003a  jmp         00000026 
    	for(int b=0;b<100;b++){
    0000003c  xor         edx,edx 
    0000003e  mov         dword ptr [ebp-4],edx 
    00000041  nop              
    00000042  jmp         00000047 
    00000044  inc         dword ptr [ebp-4] 
    00000047  cmp         dword ptr [ebp-4],64h 
    0000004b  jge         0000005A 
    		a=0x7F;
    0000004d  mov         dword ptr ds:[0040939Ch],7Fh 
    00000057  nop              
    00000058  jmp         00000044 
    	}
    }
    0000005a  nop              
    0000005b  mov         esp,ebp 
    0000005d  pop         ebp  
    0000005e  ret              
    
    ループが複数合った場合、最初にアクセスされる部分に3命令挿入され大きなオーバーヘッドになります。
    これを防ぐためにループに先立ってアクセスしておくかローカル変数に代入することで解決できます。
    static void Managedループ先にアクセス(){
    	a=0x7F;//先立ってアクセス
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  push        eax  
    00000004  cmp         dword ptr ds:[009B2E14h],0 
    0000000b  je          00000012 
    0000000d  call        79100D81 
    00000012  xor         edx,edx 
    00000014  mov         dword ptr [ebp-4],edx 
    00000017  mov         dword ptr ds:[0040939Ch],7Fh 
    	for(int b=0;b<100;b++){
    00000021  xor         edx,edx 
    00000023  mov         dword ptr [ebp-4],edx 
    00000026  nop              
    00000027  jmp         0000002C 
    00000029  inc         dword ptr [ebp-4] 
    0000002c  cmp         dword ptr [ebp-4],64h 
    00000030  jge         0000003F 
    		a=0x7F;
    00000032  mov         dword ptr ds:[0040939Ch],7Fh 
    0000003c  nop              
    0000003d  jmp         00000029 
    	}
    }
    0000003f  nop              
    00000040  mov         esp,ebp 
    00000042  pop         ebp  
    00000043  ret              
    
    結論としてstaticフィールドはプログラム全体で共有するオブジェクトですが、小さな関数でアクセスすると余計なオーバヘッドが発生します。
    引数やローカル変数にしてもメソッド内でアクセスする回数が少ないなら問題は解決しません。
    多くのアクセスを1メソッド内で完結するとパフォーマンス低下を防ぐことが出来ます。

     

     

    • 回答としてマーク 和和和 2010年4月13日 3:48
    2010年4月13日 3:47