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

enz*_*nzi 77 c# performance micro-optimization numeric-conversion range-checking

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

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

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

我很好奇这个技巧背后的理由.单个分支指令真的比两个转换要贵uint吗?或者是否还有一些其他优化会使这个代码比另外的数字比较更快?

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

Dam*_*ver 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).

也就是说,从a 到a 的转换仅仅是记账的问题 - 从现在开始,堆栈上的值/寄存器中的值现在已知为无符号整数而不是整数.intuint

因此,一旦代码被JIT打开,两次转换应该是"空闲的",然后可以执行无符号比较操作.

  • 我有点惊讶编译器没有自动实现这种优化.(或者是吗?) (4认同)
  • @IlmariKaronen怎么可能?它不知道值的含义.C#和.NET都是非常明确的定义(不像C++),这正是那种一般来说非常不安全的"优化".更不用说JIT编译器实际上没有足够的时间来寻找这些"优化",而C#编译器本身并没有真正进行大量的优化.只需编写清晰的代码,除非您能够证明性能优势(在您真正关心性能的地方). (3认同)
  • @Luaan:嗯,是的,我明白了...问题可能是编译器不够聪明,不知道`_size`不能为负,所以它无法安全地应用优化(因为它只在` (int)_size> = 0`). (2认同)
  • @Luaan:在这种情况下也不能安全地做到,因为如果`_size> int.MaxValue`,优化也会中断.如果`_size`是非负常量,编译器*可以*进行优化,或者如果它可以从早期代码推断出`_size`的值总是在0和`int.MaxValue`之间.是的,现代编译器*do*通常执行这种数据流分析,但显然它们可以做多少限制(因为完整的问题是Turing-complete). (2认同)

Jon*_*nna 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();
}
Run Code Online (Sandbox Code Playgroud)

让我们编译这些并查看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
}
Run Code Online (Sandbox Code Playgroud)

很容易看出第二个代码较少,分支较少.

实际上,根本没有演员表,可以选择是否使用blt.sbge.s使用blt.s.un,后者将传递为未签名的整数视为前者,而前者将其视为已签名.

(注意那些不熟悉CIL的人,因为这是带有CIL答案的C#问题bge.s,blt.s并且blt.s.un分别是"短"版本bge,blt并且blt.un分别blt从堆栈和分支中弹出两个值,如果第一个小于第二个,则将它们视为有符号值,同时blt.un弹出堆栈和分支的两个值,如果第一个小于第二个,则将它们视为无符号值).

这完全是微观选择,但有时微观选择值得做.进一步考虑一下,对于方法体中的其余代码,这可能意味着内联的抖动限制内的某些内容之间的差异,以及如果他们需要帮助抛出超出范围的异常,他们就是可能试图确保内联发生,如果可能的话,额外的4个字节可以产生重大影响.

实际上,与一个分支的减少相比,内联差异很可能是一个更大的交易.为了确保内联发生是值得的,并不是很多时候,但是一个如此大量使用的核心方法List<T>肯定会是其中之一.

  • 我希望语言包含一个构造来测试变量是否在一定范围内,因此不必猜测优化器将会或不会做什么.如果像这样的微优化可以使某些系统上的紧密循环速度加倍(完全可能,如果它推动"内在"决策阈值),那么它可能非常值得做; 但是,如果它不会在*any*系统上提供任何真正的加速,那么应该使用更易读的形式.当表达式*的最易读形式可能*与最快的形式相比时,我发现它很令人厌烦,但可能不会. (2认同)

DrK*_*och 8

假设_size是一个整数,列表index是私有的,并且是该函数的参数,需要测试其有效性.

进一步假设_size总是> = 0.

然后原始测试将是:

if(index < 0 || index > size) throw exception
Run Code Online (Sandbox Code Playgroud)

优化的版本

if((uint)index > (uint)_size) throw exception
Run Code Online (Sandbox Code Playgroud)

有一个比较(因为前一个例子选择了两个int.)因为演员只是重新解释这些位并使其>实际上是无符号比较,所以没有使用额外的CPU周期.

它为什么有效?

只要index> = 0,结果就是简单/平凡.

如果index <0 (uint)index则会将其变为非常大的数字:

示例:0xFFFF为-1,为int,但为65535为uint,因此

(uint)-1 > (uint)x 
Run Code Online (Sandbox Code Playgroud)

如果x是积极的,那么总是如此.

  • 在`checked`上下文中,您会得到`OverflowException`.在`unchecked`上下文中,您不能依赖于结果:_"转换的结果是目标类型的未指定值."_ http://stackoverflow.com/questions/22757239/double-maxvalue-to-integer -is阴性 (2认同)

xan*_*tos 8

请注意,如果您的项目checked不是,那么这个技巧将不起作用unchecked.最好的情况是它会更慢(因为每个演员都需要检查溢出)(或者至少不是更快),最坏的情况是你会得到一个OverflowException如果你试图传递-1作为index(而不是你的例外).

如果你想"正确地"写它并以更"肯定会工作"的方式,你应该放一个

unchecked
{
    // test
}
Run Code Online (Sandbox Code Playgroud)

在测试周围.


usr*_*usr 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检查失败.