通过转换为uint而不是检查负值来执行范围检查是否更有效?

时间:2015-03-30 10:10:51

标签: c# performance micro-optimization numeric-conversion range-checking

我在.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要贵吗?或者还有其他一些优化会使这段代码比其他数字比较更快吗?

要解决房间里的大象:是的,这是微优化,不,我不打算在我的代码中到处使用它 - 我只是好奇;)

7 个答案:

答案 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)。

也就是说,从intuint转换仅仅是记账的问题 - 从现在开始,堆栈上的值/在现在已知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.sbge.s还是使用blt.s.un,后者对待整数作为无符号传递,而前者将它们视为已签名。

(注意那些不熟悉CIL的人,因为这是带有CIL答案的C#问题,bge.sblt.sblt.s.un是&#34;短&#34;版本分别为bgebltblt.unblt在堆栈中弹出两个值,如果第一个小于第二个值则将其作为有符号值进行分支{ {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];
}