【问题标题】:How to optimize range checking for integer intervals symmetric around zero in C?如何优化 C 中围绕零对称的整数区间的范围检查?
【发布时间】:2011-05-01 09:02:52
【问题描述】:

有什么办法可以优化下面这行C代码(避免分支)?

if (i < -threshold || i > threshold) { 
    counter++; 
}

所有变量都是 16 位有符号整数。优化后的版本应该是高度可移植的。

【问题讨论】:

  • 你说“两者”,但有三个变量。
  • 不记得这是否确实有效,但请尝试if((unsigned int)i &gt; threshold)
  • @zdav 它绝对不适用于大多数编译器。这样的强制转换至少是实现定义的,通常可以得到 2 的补码。
  • @Pascal:错误。到无符号的转换完全由语言规范定义。但是,它的定义与 zdav 的想法不同(它不是绝对值)。
  • @R.. 嘿,放轻松,其他人在指出错误时不觉得有必要使用BOLD AND CAPITALS

标签: c optimization math


【解决方案1】:

以下内容如何:

counter += (i < -threshold) | (i > threshold);

假设原始代码是有效的,那么这也应该可以以可移植的方式工作。标准规定关系运算符(&lt;&gt; 等)在成功时返回等于 1int,在失败时返回等于 0

更新

下面回答Sheen的评论,代码如下:

int main()
{
    short threshold = 10;
    short i = 20;
    short counter = 0;
    
    counter += (i < -threshold) | (i > threshold);
    
    return 0;
}

在 x86 上使用 GCC 产生以下反汇编程序,没有优化:

  push   %rbp
  mov    %rsp,%rbp
  movw   $0xa,-6(%rbp)
  movw   $0x14,-4(%rbp)
  movw   $0x0,-2(%rbp)
  movswl -4(%rbp),%edx
  movswl -6(%rbp),%eax
  neg    %eax
  cmp    %eax,%edx
  setl   %dl
  movzwl -4(%rbp),%eax
  cmp    -6(%rbp),%ax
  setg   %al
  or     %edx,%eax
  movzbw %al,%dx
  movzwl -2(%rbp),%eax
  lea    (%rdx,%rax,1),%eax
  mov    %ax,-2(%rbp)
  mov    $0x0,%eax
  leaveq 
  retq  

【讨论】:

  • 不明白这是如何防止分支的。你能把生成的汇编代码贴在这里吗?
  • 如果阈值 阈值和 i = 0 可能是安全的,但如果是这样,OP 应该编辑以添加此假设。
  • @Sheen 在 x86 上,可以使用指令 setlsetg 将条件评估为整数,这有点贵,因为不常见但仍然比错误预测的分支便宜得多。
  • 一般来说这是一种糟糕的编程习惯,但如果在 CUDA 上需要这样做,我会立即使用 Oli 的方法。
  • 你测试过按位或|和逻辑或||之间的区别吗?
【解决方案2】:

有一个使用单个比较指令进行范围检查的标准习惯用法。它是这样的:

(unsigned)x - a <= (unsigned)b - a   /* a <= x <= b */
(unsigned)x - a < (unsigned)b - a    /* a <= x < b */

作为一个常见的例子(这个版本如果isdigit被标准保证是正确的):

(unsigned)ch - '0' < 10

如果您的原始类型大于int(例如long long),那么您将需要使用更大的无符号类型(例如unsigned long long)。如果ab 是常量或者已经有无符号类型,或者如果你知道b-a 不会溢出,你可以省略b 的转换。

为了使此方法起作用,您自然必须拥有a&lt;=b,并且类型/值必须使得原始表达式(即a &lt;= x &amp;&amp; x &lt;= b 或类似的)在数学上表现正确。例如,如果x 已签名而b 未签名,则x&lt;=bx=-1b=UINT_MAX-1 时可能评估为假。只要您的原始类型都是有符号的或小于您转换为的无符号类型,这不是问题。

至于这个“技巧”是如何工作的,纯粹是确定在对UINT_MAX+1进行减模后,x-a是否在0到b-a的范围内。

在你的情况下,我认为以下应该可以正常工作:

(unsigned)i + threshold > 2U * threshold;

如果threshold 在循环迭代之间没有变化,编译器可能会将threshold2U*threshold 都保留在寄存器中。

说到优化,一个好的编译器应该优化你的原始范围测试,以在它知道满足约束的情况下使用无符号算术。我怀疑很多人使用ab 常量这样做,但可能不是使用更复杂的表达式。不过,即使编译器可以对其进行优化,(unsigned)x-a&lt;b-a 习语在您希望确保 x 只被评估一次的宏中仍然非常有用。

【讨论】:

【解决方案3】:

哦,太糟糕了,这个问题已经被回答了。套用奥利的回答,代码

#include <stdint.h>
int main()
{
    int32_t threshold_square = 100;
    int16_t i = 20;
    int16_t counter = 0;

    counter += ( (int32_t) i * i > threshold_square);

    return 0;
}

使用 GCC 生成以下 x86 汇编程序,无需优化

pushq   %rbp
movq    %rsp, %rbp
movl    $100, -8(%rbp)
movw    $20, -2(%rbp)
movw    $0, -4(%rbp)
movswl  -2(%rbp),%edx
movswl  -2(%rbp),%eax
imull   %edx, %eax
cmpl    -8(%rbp), %eax
setg    %al
movzbl  %al, %edx
movzwl  -4(%rbp), %eax
leal    (%rdx,%rax), %eax
movw    %ax, -4(%rbp)
movl    $0, %eax
leave
ret

这比使用(i &lt; -threshold) | (i &gt; threshold) 少四条指令。

这是否更好当然取决于架构。

(stdint.h 的使用仅用于说明目的,严格的 C89 替换为与目标系统相关的任何内容。)

【讨论】:

  • +1:我完全没有想到这一点。不错(事后看来,很明显)的方法!
  • 尽管这是正确的,并且比 Oli 的方法更优化,但他的方法(以及出现在其他答案中的变体)的一个优点是很容易扩展它以检查不对称范围,而在这里范围总是对称的。
【解决方案4】:

我认为 Oli Charlesworth 的想法是正确的。但是,我怀疑它可以进一步优化(以牺牲可读性为代价)。

阈值可以归一化为零以消除比较。

也就是说……

counter += ((unsigned) (i + threshhold)  < (unsigned) (threshhold + threshhold));

【讨论】:

  • 这些添加中的任何一个都可能溢出。
  • Oli 是对的,但它很容易修复。在添加之前转换为unsigned,然后就可以了。由于原始值适合signed int,因此可以正常工作。
  • @R:在使用二进制补码算法的系统上,将负整数转换为无符号会添加 (UINT_MAX+1),但我相信标准明确允许系统使用符号+幅度格式,在这种情况下,强制转换将从 ((UINT_MAX+1)/2) 中减去该值。不幸的是,当总和可能位于 INT_MAX 和 UINT_MAX 之间时,我不知道有任何保证可移植的方法来将可能为负的值添加到无符号值。
  • @supercat 当心,R.. 在将负符号整数转换为无符号整数的问题上似乎非常敏感。但他是对的。它由标准定义:它添加 UINT_MAX+1 直到数字在正确的范围内。这是实现定义的从无符号到有符号的转换。
【解决方案5】:

您可以使用以下技巧将分支减少为单个分支:

if (((unsigned) (i + threshold)) > (threshold << 1)) 
{ 
  counter++; 
}

或者,对于迂腐的:

if (((unsigned) i + (unsigned) threshold) > ((unsigned) threshold << 1)) 
{ 
  counter++; 
}

【讨论】:

  • 加法(和左移)可能溢出。此外,原始代码中应该只有一个分支(嗯,我想取决于指令集)。
  • @Oli:如果原件没有溢出,它不可能溢出。如果左移溢出,那么原始测试(i &lt; -threshold) || (i &gt; threshold) 将没有意义。这行得通。我用过很多次。这是一个不明显的调整。
  • @Skizz:我同意这在实践中适用于二进制补码算术。但从技术上讲,整数溢出的行为是未定义的。这可能发生在您的代码中,例如阈值 = INT_MAX.
  • @Oli:这取决于 int 的大小,因为
  • @Skizz:如果您要将每个输入都转换为无符号,那么我会对此代码 sn-p 感到更高兴!
【解决方案6】:

根据 'i' 值的分布,您的 CPU 可能会比您可能进行的任何代码更改更好地为您缓存分支预测。有关有趣的文章,请参阅 http://igoro.com/archive/fast-and-slow-if-statements-branch-prediction-in-modern-processors/。 Reddit 讨论在这里:http://www.reddit.com/r/programming/comments/c7ues/fast_and_slow_ifstatements_branch_prediction_in/

【讨论】:

    【解决方案7】:

    这是基于bit twiddling hacks,(强烈推荐)

    #define CHAR_BIT 8
    
    int main()
    {
      int i=-3; // example input
      int treshold=2; // example treshold
      int count=0;
      // step 1: find the absolute value of i
      unsigned int r;  // the result goes here 
      int const mask = i >> (sizeof(int) * CHAR_BIT - 1);
      r = (i + mask) ^ mask;
      // step 2: compute the sign of the difference
      // sign becomes 0 (if r<=treshold)
      // sign becomes 1 otherwise
      int sign = 1 ^ ((unsigned int)(r-treshold-1) >> (sizeof(int) * CHAR_BIT - 1));
      count+=sign;
      return count;
    }
    

    这适用于 32 位整数,适应 16 位应该很容易。 它使用 g++ 编译。

    速度取决于使用的处理器。毕竟分支可能会更快。

    【讨论】:

    • 负数右移由实现定义。
    • 来自 bit twiddling hacks 网站:2003 年 3 月 7 日,Angus Duggan 指出 1989 年 ANSI C 规范保留了已定义的签名右移实现的结果,因此在某些系统上,这种 hack 可能不行。我已经读过 ANSI C 不需要将值表示为二进制补码,因此它可能也无法正常工作(在少数仍然使用补码的旧机器上)。因此,这取决于 OP 希望回答问题的便携程度。
    • @Oli,你说得对,右移负数是实现定义的。如果您发现一个编译器没有将其实现为重要位的复制(例如,每个人都期望的),我会给您发酒。(不,您自己编写的编译器不适用)
    • @Nils:不,我想不出一个!但我知道有些处理器没有算术移位指令,所以我可以想象这样一个平台的编译器可能不会为手动符号扩展而烦恼(这必须至少需要几个额外的周期)。
    【解决方案8】:

    此代码没有高度可移植的分支(但是,abs 的实现可能有一个)。

    #include <stdlib.h>
    counter += abs(i) > threshold;
    

    这是最简单的符合标准的表达式。

    如果您的编译器没有为 abs() 使用优化宏,您可以使用自己的宏/内联函数。

    这些示例使用大多数机器上使用的二进制补码格式的性质:

    #define ABS(x) ((x)*(((x)>>15)|1))
    
    #define ABS(x) ((x)-((x)>>15)^((x)>>15))
    

    您也可以将比较运算符替换为如下表达式:

    #define LESS(x, y) (-((x)-(y))>>15))
    

    结果代码:

    counter -= ((threshold - abs(i)) >> 15);
    

    所有这些宏都依赖于事实,即右移到位数减去正值或零的值为零,而负值的计算结果为负一。但这就是定义的实现。

    【讨论】:

      【解决方案9】:

      比较两者的绝对值

      short imask = i >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0
      short tmask = threshold >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0
      
      short iabsolute = (i + imask) ^ imask; // compute i absolute
      short tabsolute = (threshold + tmask) ^ tmask; // compute threshold absolute
      
      counter += iabsolute > tabsolute;
      

      【讨论】:

      • 右移一个负数是 UB。提问者要求“便携”。
      • 不错。 C99 在limits.h 中有CHAR_BIT 而不是8,以使其适用于不寻常的(但仍然是2 的补码)架构。此外,您的意思可能是使用“absolute>threshold”。
      • @Oli Charlesworth 不,它是实现定义的。 6.5.7.5.
      【解决方案10】:

      原始代码有什么问题?真的需要手动优化吗?

      任何体面的编译器都应该能够很好地优化它。任何手动优化都可能只会导致混淆。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-12-09
        • 1970-01-01
        • 2013-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多