.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つの変換よりも本当に高価ですか?または、このコードを追加の数値比較よりも高速にする他の最適化が行われていますか?
部屋の象に対処するには:はい、これはマイクロ最適化です。いいえ、コード内のどこでもこれを使用するつもりはありません。私は好奇心旺盛です;)
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つの変換は「無料」になり、符号なし比較演算を実行できるようになります。
私たちが持っているとしましょう:
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.s
とbge.s
を使用するか、blt.s.un
を使用するかを選択できます。後者は、渡された整数を符号なしとして扱い、前者は整数として扱います。署名した。
(これはCILに精通していない人のための注意、これはCILの回答を伴うC#の質問なので、bge.s
、blt.s
およびblt.s.un
はbge
の「短い」バージョンです、blt
およびblt.un
それぞれblt
は、スタックから2つの値をポップし、blt.un
がポップされている間に、最初の値が2番目の値よりも小さい場合に、それらを符号付きの値と見なして分岐します。スタックの2つの値と、それらを符号なしの値と見なしたときに最初の値が2番目の値よりも小さい場合に分岐します)。
これは完全にマイクロオプトですが、マイクロオプトを実行する価値がある場合があります。さらに検討してください。メソッド本体の残りのコードでは、インライン化のジッター制限内にあるものとそうでないものとの違いを意味する可能性があり、範囲外の例外をスローするヘルパーが必要な場合は、おそらく、可能な場合は必ずインライン化が行われるようにし、余分な4バイトがすべての違いを生む可能性があります。
実際、そのインライン化の違いは、1つのブランチの削減よりもはるかに大きな問題になる可能性が非常に高いです。インライン化が行われることを保証するために邪魔にならないことは価値があることは多くありませんが、List<T>
のような頻繁に使用されるクラスのコアメソッドは確かにその1つです。
プロジェクトがchecked
ではなくunchecked
である場合、このトリックは機能しないことに注意してください。最良の場合は遅くなります(各キャストのオーバーフローをチェックする必要があるため)(または少なくとも速くないため)、最悪の場合、OverflowException
として-1を渡そうとすると、index
が返されます(例外ではありません) 。
あなたがそれを「正しく」そしてより「確実に機能する」方法で書きたいなら、あなたは
unchecked
{
// test
}
すべてのテスト。
_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です。
はい、これはより効率的です。 範囲チェック配列アクセスの場合、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
_とチェックは失敗します。
どうやら実際の生活ではそれは速くありません。これを確認してください: 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" );
}
}
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];
}