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


77

我在.NET的List源代码中偶然发现了这段代码

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

显然,这比(?)更有效 if (index < 0 || index >= _size)

我对这招背后的理由感到好奇。一条分支指令真的比两次转换贵uint吗?还是正在进行其他优化,以使此代码比其他数字比较快?

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


4
只需几行代码即可轻松满足这种情况下的好奇心。测试一下。
山姆·阿克斯

3
@SamAxe测试只能确认强制转换更快(如果可以),而不能解释原因。
enzi 2015年

7
到uint的两个“转换”是免费的-相同的位模式将存在于同一寄存器(或内存中,但如果您幸运的话,在寄存器中)
Damien_The_Unbeliever 2015年

2
相关文章(其还包括从铸造intuint):codeproject.com/Articles/8052/...
添Schmelter

5
这种编码方式很容易引入安全漏洞。事实上,这种情况发生的所有C ++中的时间,并且是这种编码方式在C#强烈劝阻的很大一部分原因。上面的代码仅能正常工作,因为开发人员知道 _size永远不会是负数。
BlueRaja-Danny Pflughoeft 2015年

Answers:


56

MS Partition I的12.1节(支持的数据类型)中:

有符号整数类型(int8,int16,int32,int64和本机int)及其对应的无符号整数类型(无符号int8,无符号int16,无符号int32,无符号int64和本机无符号int)的区别仅在于整数的位如何不同被解释。对于将无符号整数与有符号整数区别对待的那些操作(例如,比较或带溢出的算术运算),有单独的指令将整数视为无符号(例如cgt.un和add.ovf.un)。

也就是说,从a到a的转换仅是簿记事项-从现在开始,堆栈/寄存器中的值现在被认为是无符号的int而不是int。intuint

因此,一旦将代码JITted,两次转换应该是“免费的”,然后可以执行无符号比较操作。


4
编译器没有自动实现此优化,我感到有些惊讶。(或者是?)
Ilmari Karonen

3
@IlmariKaronen怎么可能?它不知道值的含义。C#和.NET都非常明确地定义了(与C ++不同),而这恰恰是一种“优化”,通常很难做到。更不用说JIT编译器实际上没有足够的时间来寻找这些“优化”,并且C#编译器本身并没有真正进行很多优化。除非您可以证明性能优势(在您真正关心性能的地方),否则只需编写清晰的代码。
罗安2015年

2
@Luaan:嗯,是的,我明白了……问题可能在于,编译器不够聪明,无法知道_size否定的值,因此它无法安全地应用优化(因为仅当时有效(int)_size >= 0)。
Ilmari Karonen

1
在转换代码之前,这两个转换是免费的;由于仅将它们用作本地变量,所以它们之间的区别在于bltblt.sblt.un或之间blt.un.s,因此根本不需要C#生成的CIL涉及任何实际的转换。
乔恩·汉娜

2
@Luaan:在那种情况下也不能安全地这样做,因为如果,优化也会中断_size > int.MaxValue。如果编译器是一个非负常量,或者可以从早期代码中推断出的值始终在0到0(含)之间,则编译器可以进行优化。是的,现代的编译器普遍执行该类型的数据流分析,虽然很明显是有限制的,他们可以量有多大做(因为完整的问题是图灵完备)。_size_sizeint.MaxValue
Ilmari Karonen 2015年

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是“短”的版本bgebltblt.un分别blt离开本层两个值和分支如果第一小于所述第二时blt.un当将它们视为无符号值时,将它们视为有符号值,同时弹出堆栈的两个值并分支(如果第一个小于第二个值,则分支)。

完全是微型选择,但有时候值得做。进一步考虑,与方法主体中的其余代码一起,这可能意味着某些内容是否落在抖动限制(是否为内联)之内,并且如果他们不愿意使用帮助程序抛出超出范围的异常,则它们是可能会尝试尽可能确保内联,并且额外的4个字节可能会有所不同。

确实,内联差异很有可能比减少一个分支机构要大得多。没有很多时间竭尽全力确保进行内联值得,但是一类如此大量使用的核心方法List<T>肯定会是其中之一。


2
我希望语言会包含一种结构,以测试变量是否在某个范围内,因此人们不必猜测优化器将执行或将不会执行的操作。如果像这样的微优化可以使某些系统上紧密循环的速度提高一倍(如果将“内联”决策阈值微调,则完全有可能),那么这可能是非常值得做的。如果它不能在任何系统上提供真正的提速,则应该使用可读性更高的形式。当表达式的可读性最强的形式可以与最快的形式相提并论时,我可能会感到讨厌。
2015年

@supercat我大体上同意,尽管在这里它需要更广泛的知识才能知道,因为_size那时已经保证大于0 (index < 0 || index < _size) == ((uint)index >= (uint)_size)。当然,可以在优化决策过程中使用代码合同的编译器当然可以做这样的事情,但是即使进行优化以克服内联限制(移动目标的内在限制),在某些方面本身也是一个特例。 。
乔恩·汉娜

@supercat实际上,现在我想到了,如果C#具有类似的构造,例如0 < index < _size(例如使用Python,甚至对于C#来说似乎很合理,因为它没有隐式地在布尔和整数类型之间转换),那么这里的优化仍然是对使用它。
乔恩·汉娜

我希望语言包含的一件事是“(n-1)位'自然数'”变量/参数类型,该变量/参数类型会像普通符号类型一样在算术表达式中起作用,但是带有编译器强制的断言,即它们不能是负面的。通常,对无符号类型的值进行理性数学运算的唯一方法是使用下一个较大的有符号整数类型,该类型可能很讨厌且效率低下,但是表达变量/参数仅保留自然数的想法将很有帮助。
supercat 2015年

8

请注意,如果您的项目checked不是,则此技巧将无效unchecked。最好的情况是它会变慢(因为每个强制转换都需要检查是否溢出)(或至少不是更快),最坏的情况是OverflowException如果您尝试通过-1作为index(而不是您的例外),您将得到一个。

如果您想“正确”地编写它,并且以一种“一定会起作用”的方式写,则应在

unchecked
{
    // test
}

各地的测试。


考虑到溢出检查在芯片上的发生方式,检查溢出通常不会减慢任何速度。如果在checked上下文而不是正常情况下进行,它当然会抛出unchecked。但是,这并不能真正回答问题。
乔恩·汉娜

@JonHanna Yep ...但是要发表评论的时间太长了,已经有了很好的回应。速度方面:如果您查看cmp dword ptr [rsp+64h],0 / jl 00000000000000A2
强制转换

如果有一个演员,因为铸造intuint和存储不会造成在该级别的任何异常,所以测试必须是明确的,但这里的区别可能只是之间jljb
乔恩·汉娜

8

假设_size是一个整数,它是列表的私有数据,并且index是此函数的参数,需要对其有效性进行测试。

进一步假设该_size值始终> = 0。

然后,原始测试将是:

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

优化的版本

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

具有一个比较(如前一个示例中的两个比较。)由于强制转换只是重新解释了这些位并使>实际上是一个无符号的比较,因此不使用其他CPU周期。

为什么行得通?

只要索引> = 0,结果就是简单/简单的。

如果index <0,(uint)index它将变成一个很大的数字:

示例:0xFFFF的int值为-1,而uint的值为65535,因此

(uint)-1 > (uint)x 

如果x是肯定的,则始终为真。


2
checked上下文中,您得到一个OverflowException。在unchecked上下文中,您不能依赖结果:“转换的结果是目标类型的未指定值”。 stackoverflow.com/questions/22757239/...
蒂姆Schmelter

@Tim的引用似乎是针对floatdouble到整数类型的转换,处理取决于溢出检查上下文,因此不是int-> uint
xanatos

@xanatos:但是规则是一样的,如果强制转换失败,您将获得一个OverflowException带有选中上下文的T和MaxValue在非选中上下文中。所以这个回报uint.Maxvalueunchecked { uint ui = (uint)-1; };。但这并不能保证。如果尝试这样做,checked则会在-1常数的情况下出现编译器错误,并且OverflowException在运行时使用变量a 。顺便说一句,我指的是“如果索引<0,则(uint)索引会将其转换为非常大的数字:...。”
Tim Schmelter

@TimSchmelter:只是为了澄清一下,虽然未选中的(uint)-1等于uint.MaxValue(uint)-2但未选中的不是-等于uint.MaxValue-1两者都是“非常大”的-实际上,实际上比“更大” int.MaxValue
Ilmari Karonen 2015年

1
@TimSchmelter确实定义了这个强制转换的含义,并且正是这里所需要的。您所引用的答案引用了第6.2.1节的错误部分。在引号开始之前的几段中提到了这里的相关情况,给出的结果是“然后将源值视为目标类型的值”。
乔恩·汉纳

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,则检查失败。


您能否提供实现此目的的示例代码,因为我无法构建一个比另一个更快的示例。
Stilgar's

不知道问题出在哪里。只需将两个版本相互比较即可。释放模式,Ctrl-F5(无调试器)。每次测试基准测试大约需要1s,这样所有一次性成本和变化都会在噪声中消失。
usr

好吧,我尝试几种不同的方法(包括@nsimeonov在回答中提供的方法)并没有产生什么不同。
Stilgar

将您的代码作为问题发布到Stack Overflow,并在此处保留链接。我看看
usr


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" );

    }
}

您没有在比较与问题相同的代码。请参阅乔恩·汉纳(Jon Hanna)的答案-在许多情况下,大小很重要,而您已经完全迷失了。
Ben Voigt

我不明白 如果将实际检查设为单独的功能会不会很重要?我的观点是,由于CPU分支预测,第一种情况的执行速度更快。同样,在玩了一点之后,我们发现,越多的“超出范围”值,我们检查的第一种情况效果更好,但是,如果99%在范围内,那么第二种情况似乎会更快一些。因此,如果您在发布模式下进行编译,我们将有更多的乐趣
第二种

等一下,您报告了计时结果而未打开优化?
Ben Voigt 2015年

被控有罪。我最初使用dotnetfiddle.com,它没有关于优化的任何选择。结果令我惊讶。后来我尝试了单声道,并得到了相同的结果。经过多玩之后,我得到了非常有趣的统计数据。乔恩·汉纳(Jon Hanna)实际上提出了一个要点。差异可能是大小而不是速度,因为更多的指令可能导致调用方法被内联或不内联,这反过来可能会带来很大的不同。
nsimeonov

1

在英特尔处理器上进行探索时,我发现执行时间没有差异,可能是由于多个整数执行单元所致。

但是,当在既没有分支预测又没有整数执行单元的16MHZ实时微处理器上执行此操作时,则存在显着差异。

一百万次较慢的代码迭代耗时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];
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.