net - monaco c#




私のコードを試してみてください。 (4)

私はtry-catchの影響をテストするためのいくつかのコードを書いたが、いくつかの驚くべき結果を見た。

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

私のコンピュータでは、これは一貫して約0.96の値を出力します。

Fibo()の中でforループを次のようなtry-catchブロックで囲むと、

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

今それは一貫して0.69を印刷します... - 実際には速く走ります! しかし、なぜ?

注:これをリリース構成を使用してコンパイルし、EXEファイルを直接実行しました(Visual Studio以外)。

EDIT: Jon Skeetの優れた分析によれば、try-catchはx86 CLRに何らかの理由でCPUレジスタをこの特定のケースでもっと有利な方法で使用させていることを示しています。 私は、x64 CLRにはこの違いがなく、x86 CLRよりも速いというJonの発見を確認しました。 私はlong型ではなくFiboメソッド内でint型を使用してテストしましたが、x86 CLRはx64 CLRと同等の速さでした。

更新:この問題はRoslynによって修正されたようです。 同じマシン、同じCLRバージョン - VS 2013でコンパイルすると上記の問題は残りますが、VS 2015でコンパイルすると問題は解決します。


Jonの解説は、2つのバージョンの違いは、高速バージョンでは、スローバージョンではないローカル変数の1つを格納するためにレジスタのペア( esi,edi )を使用することです。

JITコンパイラでは、try-catchブロックを含むコードとそうでないコードのレジスタ使用に関するさまざまな前提があります。 これにより、異なるレジスタ割り当ての選択肢が作られます。 この場合、try-catchブロックでコードが優先されます。 異なるコードは反対の効果をもたらすかもしれないので、私はこれを汎用目的のスピードアップ技術として数えません。

最終的に、どのコードが最速に実行されるのかを判断するのは非常に難しいです。 レジスタの割り当てやそれに影響を与える要因などの低レベルの実装の詳細は、どのような特定の手法がより高速なコードを確実に生成するかはわかりません。

たとえば、次の2つの方法を考えてみましょう。 彼らは実際の例から適応されました:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

1つは、もう一方の一般的なバージョンです。 ジェネリック型をStructArrayと、メソッドが同一になります。 StructArrayは値型であるため、汎用メソッドのコンパイル済みバージョンを取得します。 しかし、実際の実行時間は特殊な方法よりもかなり長くなりますが、x86の場合に限ります。 x64の場合、タイミングはほぼ同じです。 他のケースでは、x64の違いも確認しました。


これは、インライン展開が悪くなったようです。 x86コアでは、ジッタにはebx、edx、esiおよびediレジスタがあり、ローカル変数の汎用格納に使用できます。 ecxレジスタは静的メソッドで使用できるようになりますが、 これを格納する必要はありません。 計算のためにeaxレジスタがしばしば必要です。 しかし、これらは32ビットのレジスタです。long型の変数の場合、レジスタのペアを使用する必要があります。 計算のためのedx:eaxと記憶のためのedi:ebxです。

これは、遅いバージョンの分解で目立つもので、ediもebxも使用されていません。

ジッタがローカル変数を格納するのに十分なレジスタを見つけられない場合は、スタックフレームからロードして格納するコードを生成する必要があります。 その結果、コードの速度が遅くなるため、レジスタの複数のコピーを使用してスーパースカラーの実行を可能にする内部プロセッサのコア最適化トリックである「レジスタの名前変更」というプロセッサの最適化が妨げられます。 同じレジスタを使用していても、複数の命令を同時に実行できます。 十分なレジスタを持たないことは、8つの余分なレジスタ(r9〜r15)を持つx64で扱われているx86コアの共通の問題です。

ジッタは別のコード生成最適化を適用するために最善を尽くします。あなたのFibo()メソッドをインライン化しようとします。 つまり、メソッドを呼び出すのではなく、Main()メソッドでインラインメソッドのコードを生成します。 非常に重要な最適化は、C#クラスのプロパティをフリーにして、フィールドのパーフォーマンスを与えることです。 メソッドコールのオーバーヘッドを避け、スタックフレームを設定すると、数ナノ秒節約されます。

メソッドがいつインライン化できるかを正確に判断するいくつかのルールがあります。 彼らは正確には文書化されていませんが、ブログの投稿に記載されています。 1つのルールは、メソッド本体が大きすぎると発生しないということです。 これはインライン展開による利益を奪ってしまいますが、L1命令キャッシュにはあまりにも多くのコードが生成されます。 ここで適用されるもう1つの厳しいルールは、try / catchステートメントが含まれているときにメソッドがインライン化されないということです。 その背後にある背景は例外の実装の詳細です。スタックフレームベースのSEH(Structure Exception Handling)に対するWindowsのビルトインサポートにつながります。

ジッタのレジスタ割り当てアルゴリズムの1つの動作は、このコードで再生することから推測できます。 ジッタがいつメソッドをインライン化しようとしているのかを認識しているようです。 1つのルールは、long型のローカル変数を持つインラインコードに対して、edx:eaxレジスタのペアのみを使用できることを示しています。 しかしediではなく:ebx。 それが呼び出しメソッドのコード生成にあまりにも有害であるため、ediとebxは両方とも重要な記憶レジスタです。

したがって、メソッド本体にtry / catchステートメントが含まれていることをジッターが知っているので、高速版を取得できます。 インライン化できないことがわかっているので、long変数の格納にはedi:ebxを簡単に使用します。 ジッタがインライン展開がうまくいかないことを前もって知りませんでしたので、遅いバージョンがあります。 メソッド本体のコードを生成したでしか見つかりませんでした。

欠陥は、それが戻ってこなかったことと、メソッドのコードを再生成したことです。 それは、それが操作しなければならない時間的制約を考えると、理解できるものです。

このスローダウンはx64では発生しません。なぜなら、x64では8つ以上のレジスタがあるからです。 もう一つの理由は、(raxのように)ただ一つのレジスタにlongを格納できるからです。 また、ジッタがピッキングレジスタの柔軟性が非常に高いため、longの代わりにintを使用するとスローダウンは発生しません。


スタック使用量の最適化を専門とするRoslynエンジニアの一人がこれを見て、C#コンパイラがローカル変数ストアを生成する方法とJITコンパイラの登録方法とのやりとりに問題があるように私に報告します対応するx86コードのスケジューリング その結果、地方自治体の荷物と店舗で最適ではないコード生成が行われます。

何らかの理由で私たち全員に不明な点がある場合、JITterがブロックがtry-protected領域にあることが分かっているときは、問題のあるコード生成パスは回避されます。

これはかなり奇妙です。 JITterチームにフォローアップを行い、バグを修正して解決できるかどうかを確認します。

また、RoslynのC#とVBコンパイラのアルゴリズムの改善により、地元の人々をいつ一時的にプッシュしてポップするかを決定しています。活性化の持続時間。 私たちはJITterが地方自治体が「死んだ」時代になると、より良いヒントを与えれば、レジスタ割り振りなどのより良い仕事をすることができると信じています。

これを私たちの注意を引くことに感謝し、奇妙な行動のためにお詫び申し上げます。


私はこれをコメントとして記しておきたいと思いますが、実際これがそうであることは確かではありませんが、思い出したように、try / except文はゴミ処理メカニズムの変更を伴わないコンパイラはスタックから再帰的にオブジェクトメモリの割り当てをクリアするという点で動作します。 この場合、消去されるオブジェクトがないか、またはforループが、ガベージコレクションメカニズムが異なるコレクションメソッドを実行するのに十分なことを認識するクロージャを構成することがあります。 おそらくそうではありませんでしたが、他の場所で議論されているのを見ていないので、言及する価値があると思いました。





performance-testing