我在.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
要贵吗?或者还有其他一些优化会使这段代码比其他数字比较更快吗?
要解决房间里的大象:是的,这是微优化,不,我不打算在我的代码中到处使用它 - 我只是好奇;)
答案 0 :(得分:55)
来自MS Partition I,第12.1节(支持的数据类型):
有符号整数类型(int8,int16,int32,int64和native int)及其对应的无符号 整数类型(unsigned int8,unsigned int16,unsigned int32,unsigned int64和native unsigned int)仅区别于整数位的解释方式。对于那些无符号整数的运算 与有符号整数的处理方式不同(例如,在比较或算术中有溢出),它们是分开的 将整数视为无符号的指令(例如,cgt.un和add.ovf.un)。
也就是说,从int
到uint
的转换仅仅是记账的问题 - 从现在开始,堆栈上的值/在现在已知register是unsigned int而不是int。
所以这两次转换应该是&#34;免费&#34;一旦代码被JIT,然后可以执行无符号比较操作。
答案 1 :(得分:29)
让我们说:
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
}
很容易看出第二个代码较少,分支较少。
实际上,根本没有演员阵容,可以选择使用blt.s
和bge.s
还是使用blt.s.un
,后者对待整数作为无符号传递,而前者将它们视为已签名。
(注意那些不熟悉CIL的人,因为这是带有CIL答案的C#问题,bge.s
,blt.s
和blt.s.un
是&#34;短&#34;版本分别为bge
,blt
和blt.un
。blt
在堆栈中弹出两个值,如果第一个小于第二个值则将其作为有符号值进行分支{ {1}}弹出堆栈和分支的两个值,如果第一个小于第二个,则将它们视为无符号值时。)
这完全是微观选择,但有时微观选择值得做。进一步考虑一下,对于方法体中的其余代码,这可能意味着内嵌与内联的抖动限制内的某些内容之间的差异,以及他们是否需要帮助抛出超出范围的异常他们& #39;如果可能的话,可能会尝试确保内联发生,而额外的4个字节可能会产生重大影响。
事实上,与一个分支的减少相比,内联差异很可能是一个更大的交易。为了确保内联发生是值得的,很多时候都没有,但是像blt.un
这样大量使用的核心方法肯定会是其中之一。
答案 2 :(得分:8)
假设_size
是一个整数,对列表是私有的,index
是该函数的参数,需要测试其有效性。
进一步假设_size
总是&gt; = 0。
然后原始测试将是:
if(index < 0 || index > size) throw exception
优化版本
if((uint)index > (uint)_size) throw exception
有一个比较(因为前一个例子选择了两个int。)因为强制转换只是重新解释这些位并使>
实际上是无符号比较,所以没有使用额外的CPU周期。
为什么会有效?
只要index&gt; = 0,结果就很简单/无关紧要。
如果索引&lt; 0 (uint)index
会把它变成一个非常大的数字:
示例:0xFFFF为-1,为int,但为65535为uint,因此
(uint)-1 > (uint)x
如果x
为肯定,则始终为真。
答案 3 :(得分:8)
请注意,如果您的项目为checked
而不是unchecked
,则此技巧不会有效。最好的情况是它会更慢(因为每个演员都需要检查溢出)(或至少不是更快),最坏的情况下,如果你试图传递-1 {{1},你会得到OverflowException
(而不是你的例外)。
如果你想写'#34;正确&#34;并且在更多&#34;肯定会工作&#34;方式,你应该放一个
index
所有测试。
答案 4 :(得分:5)
是的,这样效率更高。当范围检查数组访问时,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 :(得分:4)
显然在现实生活中它并不快。检查一下:https://dotnetfiddle.net/lZKHmn
事实证明,由于英特尔的分支预测和并行执行,更明显和可读的代码实际上工作得更快......
以下是代码:
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" );
}
}
答案 6 :(得分:1)
在intel处理器上进行探索时,我发现执行时间没有差异,可能是由于多个整数执行单元。
但是当在16MHZ实时微处理器上执行此操作时,既没有分支预测也没有整数执行单元,则存在显着差异。
慢速代码的100万次迭代耗时1761 ms
int slower(char *a, long i)
{
if (i < 0 || i >= 10)
return 0;
return a[i];
}
100万次迭代更快的代码耗时1635 ms
int faster(char *a, long i)
{
if ((unsigned int)i >= 10)
return 0;
return a[i];
}