4

仮想メンバーと封印されたメンバーのパフォーマンスをいじくり回しています。

以下は私のテストコードです。

出力は

virtual total 3166ms
per call virtual 3.166ns
sealed total 3931ms
per call sealed 3.931ns

これによると、仮想呼び出しは封印された呼び出しよりも速いので、私は何か間違ったことをしているに違いありません。

「コードの最適化」をオンにしてリリースモードで実行しています。

編集:VSの外部で(コンソールアプリとして)実行している場合、時間は猛暑に近づいています。しかし、ほとんどの場合、仮想は前面に出てきます。

[TestFixture]
public class VirtTests
{

    public class ClassWithNonEmptyMethods
    {
        private double x;
        private double y;

        public virtual void VirtualMethod()
        {
            x++;
        }
        public void SealedMethod()
        {
            y++;
        }
    }

    const int iterations = 1000000000;


    [Test]
    public void NonEmptyMethodTest()
    {

        var foo = new ClassWithNonEmptyMethods();
        //Pre-call
        foo.VirtualMethod();
        foo.SealedMethod();

        var virtualWatch = new Stopwatch();
        virtualWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.VirtualMethod();
        }
        virtualWatch.Stop();
        Console.WriteLine("virtual total {0}ms", virtualWatch.ElapsedMilliseconds);
        Console.WriteLine("per call virtual {0}ns", ((float)virtualWatch.ElapsedMilliseconds * 1000000) / iterations);


        var sealedWatch = new Stopwatch();
        sealedWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.SealedMethod();
        }
        sealedWatch.Stop();
        Console.WriteLine("sealed total {0}ms", sealedWatch.ElapsedMilliseconds);
        Console.WriteLine("per call sealed {0}ns", ((float)sealedWatch.ElapsedMilliseconds * 1000000) / iterations);

    }

}
4

4 に答える 4

4

コード効率に対するメモリアライメントの影響をテストしています。32ビットJITコンパイラは、サイズが32ビットを超え、C#コードでlongおよびdoubleの値型の効率的なコードを生成するのに問題があります。問題の根本は32ビットGCヒープアロケータであり、4の倍数であるアドレスに割り当てられたメモリのアラインメントを約束するだけです。これがここでの問題です。2倍にインクリメントしています。doubleは、8の倍数のアドレスにアラインされる場合にのみ効率的です。スタックと同じ問題です。ローカル変数の場合、32ビットマシンでは4にのみアラインされます。

L1 CPUキャッシュは、「キャッシュライン」と呼ばれるブロックで内部的に編成されています。プログラムが位置ずれしたdoubleを読み取ると、ペナルティが発生します。特にキャッシュラインの終わりにまたがる場合、2つのキャッシュラインからのバイトを読み取って結合する必要があります。32ビットジッターではミスアラインメントは珍しくありません。「x」フィールドがたまたま8の倍数のアドレスに割り当てられるのは50〜50オッズです。そうでない場合は、「x」と「y」はずれてしまい、そのうちの1つがキャッシュラインにまたがる可能性があります。テストを作成した方法では、VirtualMethodまたはSealedMethodのいずれかが遅くなります。同等の結果を得るには、必ず同じフィールドを使用するようにしてください。

同じことがコードにも当てはまります。仮想テストと封印されたテストのコードを交換して、結果を任意に変更します。そのようにして、封印されたテストをかなり速くすることに問題はありませんでした。速度のわずかな違いを考えると、おそらくコードの配置の問題を見ているでしょう。x64ジッターは、分岐ターゲットを整列させるためにNOPを挿入しようとしますが、x86ジッターはそうではありません。

また、少なくとも20回、ループ内でタイミングテストを数回実行する必要があります。その後、ガベージコレクターがクラスオブジェクトを移動した場合の影響も観察される可能性があります。ダブルは後で異なる配置になり、タイミングが劇的に変わる可能性があります。longやdoubleなどの64ビット値型の値にアクセスするには、3つの異なるタイミングがあり、8に整列され、キャッシュライン内で4に整列され、2つのキャッシュラインにわたって4に整列されます。速いものから遅いものへ。

ペナルティは厳しく、キャッシュラインにまたがるダブルの読み取りは、整列されたダブルの読み取りよりも約3倍遅くなります。また、double [](doubleの配列)が1000個の要素しかない場合でも、通常のしきい値である80KBのかなり南にある場合でも、double [](doubleの配列)が割り当てられる主な理由は、LOHのアライメント保証が8です。これらのアライメントの問題x64ジッターによって生成されたコードでは完全に消え、スタックとGCヒープの両方の配列は8になります。

于 2010-12-11T05:30:10.517 に答える
1

まず、メソッドをマークする必要がありますsealed

次に、override仮想メソッドにを提供します。派生クラスのインスタンスを作成します。

3番目のテストとして、sealed overrideメソッドを作成します。

これで、比較を開始できます。

編集:おそらくこれをVSの外部で実行する必要があります。

アップデート:

私が言っていることの例。

abstract class Foo
{
  virtual void Bar() {}
}

class Baz : Foo
{
  sealed override void Bar() {}
}

class Woz : Foo
{
  override void Bar() {}
}

次に、とBarのインスタンスの呼び出し速度をテストします。また、アセンブリ外のメンバーとクラスの可視性がJIT分析に影響を与える可能性があると思います。BazWoz

于 2010-12-10T06:32:56.843 に答える
1

いくつかの初期費用が発生している可能性があります。Test-A / Test-Bコードをループでラップして、数回実行してみてください。また、ある種の順序付け効果が見られる場合もあります。それ(およびループ効果の上部/下部)を回避するには、2〜3回展開します。

于 2010-12-11T03:14:27.297 に答える
0

次のコードをテストの参照として使用して、Ildasm.exe(IL逆アセンブラー)ツールを使用してコンパイラーによって生成されたMicrosoft中間言語(MSIL)情報を分析してみましょう。

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

このツールを実行するには、Visual Studioの開発者コマンドプロンプトを開き、コマンドildasmを実行します。

**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.9.13
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************


C:\Program Files (x86)\Microsoft Visual Studio\2017\Community>ildasm

アプリケーションが起動したら、前のアプリケーションの実行可能ファイル(またはアセンブリ)をロードします

この画像に代替テキストはありませんMainメソッドをダブルクリックして、Microsoft中間言語(MSIL)情報を表示します。

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

ご覧のとおり、各クラスはnewobjを使用して、オブジェクト参照をスタックにプッシュすることで新しいインスタンスを作成し、 callvirtはそれぞれのオブジェクトのDoStuff()メソッドのレイトバウンドを呼び出します。

この情報から判断すると、封印されたクラス、派生クラス、および基本クラスの両方が、コンパイラーによって同じ方法で管理されているようです。念のため、VisualStudioの逆アセンブリウィンドウを使用してJITでコンパイルされたコードを分析してさらに詳しく見ていきましょう。

[ツール]>[オプション]>[デバッグ]>[一般]で、[アドレスレベルのデバッグを有効にする]を選択して逆アセンブリを有効にします。

この画像に代替テキストはありませんアプリケーションの開始時にブレーキポイントを設定し、デバッグを開始します。アプリケーションがブレーキポイントに達したら、[デバッグ]>[ウィンドウ]>[逆アセンブリ]を選択して、[逆アセンブリ]ウィンドウを開きます。

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

前のコードでわかるように、オブジェクトの作成は同じですが、封印されたクラスと派生クラス/基本クラスのメソッドを呼び出すために実行される命令は少し異なります。データをRAMのレジスタに移動した後(mov命令)、封印されたメソッドを呼び出し、実際にメソッドを呼び出す前に、dword ptr [ecx]とecx(cmp命令)の比較を実行します。

Torbj¨ornGranlundが作成したレポートによると、AMDおよびIntel x86プロセッサの命令レイテンシとスループットによると、IntelPentium4での次の命令の速度は次のとおりです。

  • mov:レイテンシとして1サイクルあり、プロセッサはこのタイプのサイクルごとに2.5命令を維持できます
  • cmp:レイテンシとして1サイクルあり、プロセッサはこのタイプのサイクルごとに2つの命令を維持できます

結論として、今日のコンパイラとプロセッサの最適化により、封印されたクラスと封印されていないクラスの間のパフォーマンスは基本的に非常に少なくなり、ほとんどのアプリケーションとは無関係になりました。

参考文献

于 2020-03-02T16:15:57.180 に答える