web-dev-qa-db-ja.com

負の値をチェックする代わりにuintにキャストして範囲チェックを実行する方が効率的ですか?

.NETの List source code でこのコードに遭遇しました。

_// Following trick can reduce the range check by one
if ((uint) index >= (uint)_size) {
  ThrowHelper.ThrowArgumentOutOfRangeException();
}
_

どうやらこれはif (index < 0 || index >= _size)よりも効率的(?)

トリックの背後にある理論的根拠に興味があります。単一の分岐命令は、uintへの2つの変換よりも本当に高価ですか?または、このコードを追加の数値比較よりも高速にする他の最適化が行われていますか?

部屋の象に対処するには:はい、これはマイクロ最適化です。いいえ、コード内のどこでもこれを使用するつもりはありません。私は好奇心旺盛です;)

77
enzi

MSパーティションI 、セクション12.1(サポートされているデータタイプ)から:

符号付き整数型(int8、int16、int32、int64、およびネイティブint)とそれに対応する符号なし整数型(unsigned int8、unsigned int16、unsigned int32、unsigned int64、およびネイティブunsigned int)は、整数のビットの違いのみが異なります解釈されます。符号なし整数が符号付き整数とは異なる方法で処理される演算(たとえば、比較やオーバーフローを伴う算術演算)では、整数を符号なしとして処理するための個別の命令(たとえば、cgt.unおよびadd.ovf.un)があります。

つまり、intからuintへの変換は、単なる簿記の問題です。これからは、 stack/inレジスタは、intではなくunsigned intとして認識されるようになりました。

したがって、コードがJITされると、2つの変換は「無料」になり、符号なし比較演算を実行できるようになります。

55

私たちが持っているとしましょう:

public void TestIndex1(int index)
{
  if(index < 0 || index >= _size)
    ThrowHelper.ThrowArgumentOutOfRangeException();
}
public void TestIndex2(int index)
{
  if((uint)index >= (uint)_size)
    ThrowHelper.ThrowArgumentOutOfRangeException();
}

これらをコンパイルして、ILSpyを見てみましょう。

.method public hidebysig 
    instance void TestIndex1 (
        int32 index
    ) cil managed 
{
    IL_0000: ldarg.1
    IL_0001: ldc.i4.0
    IL_0002: blt.s IL_000d
    IL_0004: ldarg.1
    IL_0005: ldarg.0
    IL_0006: ldfld int32 TempTest.TestClass::_size
    IL_000b: bge.s IL_0012
    IL_000d: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException()
    IL_0012: ret
}

.method public hidebysig 
    instance void TestIndex2 (
        int32 index
    ) cil managed 
{
    IL_0000: ldarg.1
    IL_0001: ldarg.0
    IL_0002: ldfld int32 TempTest.TestClass::_size
    IL_0007: blt.un.s IL_000e
    IL_0009: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException()
    IL_000e: ret
}

2番目のコードはコードが1つ少なく、ブランチが1つ少ないことが簡単にわかります。

本当にキャストはありません。blt.sbge.sを使用するか、blt.s.unを使用するかを選択できます。後者は、渡された整数を符号なしとして扱い、前者は整数として扱います。署名した。

(これはCILに精通していない人のための注意、これはCILの回答を伴うC#の質問なので、bge.sblt.sおよびblt.s.unbgeの「短い」バージョンです、bltおよびblt.unそれぞれbltは、スタックから2つの値をポップし、blt.unがポップされている間に、最初の値が2番目の値よりも小さい場合に、それらを符号付きの値と見なして分岐します。スタックの2つの値と、それらを符号なしの値と見なしたときに最初の値が2番目の値よりも小さい場合に分岐します)。

これは完全にマイクロオプトですが、マイクロオプトを実行する価値がある場合があります。さらに検討してください。メソッド本体の残りのコードでは、インライン化のジッター制限内にあるものとそうでないものとの違いを意味する可能性があり、範囲外の例外をスローするヘルパーが必要な場合は、おそらく、可能な場合は必ずインライン化が行われるようにし、余分な4バイトがすべての違いを生む可能性があります。

実際、そのインライン化の違いは、1つのブランチの削減よりもはるかに大きな問題になる可能性が非常に高いです。インライン化が行われることを保証するために邪魔にならないことは価値があることは多くありませんが、List<T>のような頻繁に使用されるクラスのコアメソッドは確かにその1つです。

29
Jon Hanna

プロジェクトがcheckedではなくuncheckedである場合、このトリックは機能しないことに注意してください。最良の場合は遅くなります(各キャストのオーバーフローをチェックする必要があるため)(または少なくとも速くないため)、最悪の場合、OverflowExceptionとして-1を渡そうとすると、indexが返されます(例外ではありません) 。

あなたがそれを「正しく」そしてより「確実に機能する」方法で書きたいなら、あなたは

unchecked
{
    // test
}

すべてのテスト。

8
xanatos

_sizeは整数で、リストに対してプライベートであり、indexはこの関数の引数であり、有効性をテストする必要があります。

さらに_sizeは常に> = 0です。

その後、元のテストは次のようになります。

if(index < 0 || index > size) throw exception

最適化バージョン

if((uint)index > (uint)_size) throw exception

比較は1つあります(前の例では2つでした)。キャストはビットを再解釈して>実際には、符号なし比較であり、追加のCPUサイクルは使用されません。

なぜ機能するのですか?

インデックス> = 0である限り、結果は単純/簡単です。

インデックス<0の場合、(uint)indexは非常に大きな数に変換します。

例:0xFFFFはintとして-1ですが、uintとして65535なので、

(uint)-1 > (uint)x 

xが正の場合、常にtrueです。

8
DrKoch

はい、これはより効率的です。 範囲チェック配列アクセスの場合、JITは同じトリックを実行します。

変換と推論は次のとおりです。

_i >= 0 && i < array.Length_が_(uint)i < (uint)array.Length_と同じ値になるように_array.Length <= int.MaxValue_を使用するため、_array.Length_は_(uint)array.Length_になります。 iが負の場合、_(uint)i > int.MaxValue_とチェックは失敗します。

5
usr

どうやら実際の生活ではそれは速くありません。これを確認してください: https://dotnetfiddle.net/lZKHmn

結局のところ、Intelの分岐予測と並列実行のおかげで、より明確で読みやすいコードが実際にはより高速に動作します...

これがコードです:

using System;
using System.Diagnostics;

public class Program
{


    const int MAX_ITERATIONS = 10000000;
    const int MAX_SIZE = 1000;


    public static void Main()
    {

            var timer = new Stopwatch();


            Random Rand = new Random();
            long InRange = 0;
            long OutOfRange = 0;

            timer.Start();
            for ( int i = 0; i < MAX_ITERATIONS; i++ ) {
                var x = Rand.Next( MAX_SIZE * 2 ) - MAX_SIZE;
                if ( x < 0 || x > MAX_SIZE ) {
                    OutOfRange++;
                } else {
                    InRange++;
                }
            }
            timer.Stop();

            Console.WriteLine( "Comparision 1: " + InRange + "/" + OutOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms" );


            Rand = new Random();
            InRange = 0;
            OutOfRange = 0;

            timer.Reset();
            timer.Start();
            for ( int i = 0; i < MAX_ITERATIONS; i++ ) {
                var x = Rand.Next( MAX_SIZE * 2 ) - MAX_SIZE;
                if ( (uint) x > (uint) MAX_SIZE ) {
                    OutOfRange++;
                } else {
                    InRange++;
                }
            }
            timer.Stop();

            Console.WriteLine( "Comparision 2: " + InRange + "/" + OutOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms" );

    }
}
4
nsimeonov

Intelプロセッサでこれを調査したところ、おそらく複数の整数実行ユニットが原因で、実行時間に差はありませんでした。

しかし、分岐予測も整数実行ユニットも持たない16MHZリアルタイムマイクロプロセッサでこれを行うと、顕著な違いがありました。

遅いコードの100万回の反復には1761ミリ秒かかりました

int slower(char *a, long i)
{
  if (i < 0 || i >= 10)
    return 0;

  return a[i];
}

100万回の反復処理の高速化に1635ミリ秒かかりました

int faster(char *a, long i)
{
  if ((unsigned int)i >= 10)
    return 0;
  return a[i];
}
1