22

我需要一个程序来获取两个数字中的较小者,我想知道是否使用标准“如果 x 小于 y”

int a, b, low;
if (a < b) low = a;
else low = b;

或多或少比这更有效:

int a, b, low;
low = b + ((a - b) & ((a - b) >> 31));

(或将其放在int delta = a - b顶部并替换实例的变体a - b)。

我只是想知道其中哪一个会更有效(或者差异是否太小以至于不相关),以及 if-else 语句与一般替代方案的效率。

4

16 回答 16

31

(免责声明:以下内容涉及通常不需要的非常低级的优化。如果您继续阅读,您将放弃抱怨计算机速度很快并且没有任何理由担心这类事情的权利。)

消除语句的一个优点if是可以避免分支预测惩罚。

分支预测惩罚通常仅在不易预测分支时才会出现问题。当一个分支几乎总是被采用/不被采用,或者它遵循一个简单的模式时,它很容易被预测。例如,循环语句中的分支除了最后一个之外,每次都取,所以很容易预测。但是,如果您有类似的代码

a = random() % 10
if (a < 5)
  print "Less"
else
  print "Greater"

那么这个分支就不容易预测,并且会经常导致与清除缓存和回滚在分支的错误部分执行的指令相关的预测惩罚。

避免此类惩罚的一种方法是使用三元 ( ?:) 运算符。在简单的情况下,编译器将生成条件移动指令而不是分支。

所以

int a, b, low;
if (a < b) low = a;
else low = b;

变成

int a, b, low;
low = (a < b) ? a : b

在第二种情况下,不需要分支指令。此外,它比您的位旋转实现更清晰、更具可读性。

当然,这是一个微优化,不太可能对您的代码产生重大影响。

于 2010-06-09T20:38:28.820 回答
9

简单的答案:一个条件跳转将比两个减法、一个加法、一个按位与和一个移位操作相结合更有效。 我在这一点上已经受过足够的教育(见评论),我什至没有足够的信心说它通常更有效。

务实的回答:无论哪种方式,您为额外的 CPU 周期支付的费用几乎不如程序员弄清楚第二个示例在做什么所花费的时间。程序可读性第一,效率第二。

于 2010-06-09T20:39:43.280 回答
8

在 gcc 4.3.4、amd64(core 2 duo)、Linux 上编译:

int foo1(int a, int b)
{
    int low;
    if (a < b) low = a;
    else low = b;
    return low;
}

int foo2(int a, int b)
{
    int low;
    low = b + ((a - b) & ((a - b) >> 31));
    return low;
}

我得到:

foo1:
    cmpl    %edi, %esi
    cmovle  %esi, %edi
    movl    %edi, %eax
    ret

foo2:
    subl    %esi, %edi
    movl    %edi, %eax
    sarl    $31,  %eax
    andl    %edi, %eax
    addl    %esi, %eax
    ret

...我很确定这不会计入分支预测,因为代码不会跳转。此外,非 if 语句版本要长 2 条指令。我想我会继续编码,让编译器完成它的工作。

于 2010-06-09T23:18:55.407 回答
7

最大的问题是您的第二个示例无法在 64 位机器上运行

然而,即使忽略这一点,现代编译器也足够聪明,可以在所有可能的情况下考虑无分支预测,并比较估计的速度。所以,你的第二个例子很可能实际上会更慢

if 语句和使用三元运算符之间没有区别,因为即使是大多数愚蠢的编译器也足够聪明,可以识别这种特殊情况。


[编辑]因为我认为这是一个非常有趣的话题,所以我写了一篇关于它的博客文章。

于 2010-06-09T20:59:42.113 回答
7

与任何低级优化一样,在目标 CPU/板设置上对其进行测试。

在我的编译器(x86_64 上的 gcc 4.5.1)上,第一个示例变为

cmpl    %ebx, %eax
cmovle  %eax, %esi

第二个例子变成

subl    %eax, %ebx
movl    %ebx, %edx
sarl    $31, %edx
andl    %ebx, %edx
leal    (%rdx,%rax), %esi

不确定第一个是否在所有情况下都更快,但我敢打赌。

于 2010-06-09T21:16:21.053 回答
4

无论哪种方式,程序集都只有几条指令,无论哪种方式,执行这些指令都需要皮秒。

我会分析应用程序,将您的优化工作集中在更有价值的事情上。

此外,这种优化节省的时间不值得任何试图维护它的人浪费时间。

对于像这样的简单语句,我发现三元运算符非常直观:

low = (a < b) ? a : b;

简洁明了。

于 2010-06-09T20:34:59.453 回答
4

对于这么简单的事情,为什么不尝试一下呢?

通常,您会首先进行概要分析,将其识别为热点,尝试更改,然后查看结果。

我编写了一个简单的程序,将两种传递随机数的技术(这样我们看不到完美的分支预测)与 Visual C++ 2010 进行比较。我的机器上进行 100,000,000 次迭代的方法之间的区别?总共不到 50 毫秒,并且 if 版本往往更快。查看 codegen,编译器成功地将简单的 if 转换为 cmovl 指令,完全避免了分支。

于 2010-06-09T21:18:01.627 回答
2

当你进入真正有点复杂的黑客攻击时,要警惕的一件事是它们如何与内联后发生的编译器优化交互。例如,可读过程

int foo (int a, int b) {
   return ((a < b) ? a : b);
}

在任何情况下都可能被编译成非常有效的东西,但在某些情况下它可能会更好。例如,假设有人写

int bar = foo (x, x+3);

内联后,编译器会识别出3是肯定的,然后可以利用有符号溢出未定义这一事实来完全消除测试,得到

int bar = x;

在这种情况下,编译器应该如何优化你的第二个实现就不太清楚了。当然,这是一个相当人为的例子,但类似的优化实际上在实践中很重要。当然,当性能至关重要时,您不应该接受错误的编译器输出,但是在求助于下一个经过惊人改进的编译器版本之前,看看您是否可以找到产生良好输出的清晰代码可能是明智之举能优化到死。

于 2014-06-22T05:50:34.353 回答
1

我要指出的一件事是,我没有注意到这样的优化很容易被其他问题所淹没。例如,如果您在两个大型数字数组(或者更糟糕的是,分散在内存中的成对的数字)上运行此例程,则在当今的 CPU 上获取值的成本很容易使 CPU 的执行管道停止。

于 2010-06-09T20:46:55.333 回答
1

我只是想知道其中哪一个会更有效(或者差异是否微不足道),以及 if-else 语句与一般替代方案的效率。

桌面/服务器 CPU 针对流水线进行了优化。第二个理论上更快,因为 CPU 不必分支并且可以利用多个 ALU 并行评估部分表达式。具有混合独立操作的更多非分支代码最适合此类 CPU。(但即使这样,现在也被现代“条件”CPU 指令所否定,这些指令也允许第一个代码无分支。)

在嵌入式 CPU 上,如果通常更便宜(相对于其他所有东西),它们也没有很多备用 ALU 来评估乱序操作(如果它们完全支持乱序执行)。更少的代码/数据更好 - 缓存也很小。(我什至在嵌入式应用程序中看到了冒泡排序的使用:该算法使用最少的内存/代码,并且对于少量信息来说足够快。)

重要提示:不要忘记编译器优化。使用许多技巧,编译器有时可以自己删除分支:内联、常量传播、重构等。

但最后我会说是的,差异是微不足道的。从长远来看,可读代码胜出。

从 CPU 方面的事情来看,现在花时间使代码具有多线程和 OpenCL 功能会更有价值。

于 2010-06-10T16:02:28.393 回答
0

为什么low = a;iflow = a;else?而且,为什么31?如果 31 与 CPU 字长有关,如果代码要在不同大小的 CPU 上运行怎么办?

if..else 方式看起来更具可读性。我喜欢程序对人类和编译器一样可读。

于 2010-06-09T20:40:50.360 回答
0

使用 gcc -o foo -g -p -O0, Solaris 9 v240 的配置文件结果

 %Time Seconds Cumsecs  #Calls   msec/call  Name
  36.8    0.21    0.21 8424829      0.0000  foo2
  28.1    0.16    0.37       1    160.      main
  17.5    0.10    0.4716850667      0.0000  _mcount
  17.5    0.10    0.57 8424829      0.0000  foo1
   0.0    0.00    0.57       4      0.      atexit
   0.0    0.00    0.57       1      0.      _fpsetsticky
   0.0    0.00    0.57       1      0.      _exithandle
   0.0    0.00    0.57       1      0.      _profil
   0.0    0.00    0.57    1000      0.000   rand
   0.0    0.00    0.57       1      0.      exit

代码:

int
foo1 (int a, int b, int low)        
{
   if (a < b) 
      low = a;   
   else 
      low = b;         
   return low;
}

int                       
foo2 (int a, int b, int low)
{
   low = (a < b) ? a : b;
   return low;
}


int main()
{
    int low=0;
    int a=0;
    int b=0;
    int i=500;
    while (i--)
    {
       for(a=rand(), b=rand(); a; a--)
       {
           low=foo1(a,b,low);
           low=foo2(a,b,low);
       }        
    }
    return 0;
}

根据数据,在上述环境中,与此处所述的几种信念完全相反的情况并未被发现是正确的。注意“在这种环境中”如果构造比三元更快?: 构造

于 2010-06-09T21:40:20.713 回答
0

不久前我写了一个三元逻辑模拟器,这个问题对我来说是可行的,因为它直接影响我的解释器执行速度;我被要求尽可能快地模拟成吨的三元逻辑门。

在二进制编码的三进制系统中,一个三元组被打包成两位。最高有效位表示负数,最低有效位表示正数。案例“11”不应该发生,但必须妥善处理,并以0威胁。

考虑inline int bct_decoder( unsigned bctData )函数,它应该将我们格式化的 trit 返回为常规整数 -1、0 或 1;正如我所观察到的,有 4 种方法:我称它们为“cond”、“mod”、“math”和“lut”;让我们调查一下

首先是基于 jz|jnz 和 jl|jb 条件跳转,因此 cond. 它的性能一点也不好,因为依赖于分支预测器。更糟糕的是 - 它会有所不同,因为不知道是否会有一个或两个先验分支。这是一个例子:

inline int bct_decoder_cond( unsigned bctData ) {
   unsigned lsB = bctData & 1;
   unsigned msB = bctData >> 1;
   return
       ( lsB == msB ) ? 0 : // most possible -> make zero fastest branch
       ( lsB > msB ) ? 1 : -1;
}

这是最慢的版本,在最坏的情况下可能涉及 2 个分支,这是二进制逻辑失败的地方。在我的 3770k 上,它在随机数据上平均产生大约 200MIPS。(这里和之后 - 每个测试是在随机填充的 2mb 数据集上进行 1000 次尝试的平均值)

下一个依赖于模运算符,它的速度介于第一和第三之间,但绝对更快 - 600 MIPS:

inline int bct_decoder_mod( unsigned bctData ) {
    return ( int )( ( bctData + 1 ) % 3 ) - 1;
}

下一个是无分支方法,它只涉及数学,因此是数学;它根本不假设跳转指令:

inline int bct_decoder_math( unsigned bctData ) {
    return ( int )( bctData & 1 ) - ( int )( bctData >> 1 );
}

这做了应该做的事情,并且表现得非常好。相比之下,性能估计为 1000 MIPS,比分支版本快 5 倍。由于缺乏原生 2 位有符号整数支持,分支版本可能会变慢。但在我的应用程序中,它本身就是一个很好的版本。

如果这还不够,那么我们可以走得更远,有一些特别的东西。接下来称为查找表方法:

inline int bct_decoder_lut( unsigned bctData ) {
    static const int decoderLUT[] = { 0, 1, -1, 0 };
    return decoderLUT[ bctData & 0x3 ];
}

在我的例子中,一个 trit 只占用 2 位,所以 lut 表只有 2b*4 = 8 个字节,值得一试。它适合缓存并以 1400-1600 MIPS 的速度快速运行,这就是我的测量精度下降的地方。这就是快速数学方法的 1.5 倍加速。那是因为你只有预先计算好的结果和单AND条指令。遗憾的是缓存很小并且(如果您的索引长度大于几位)您根本无法使用它。

所以我想我回答了你的问题,关于分支/无分支代码可能是什么样的。答案要好得多,并且有详细的样本、真实世界的应用和真实的性能测量结果。

于 2017-10-22T13:25:55.893 回答
0

更新了采用当前(2018)编译器矢量化状态的答案。对于矢量化不是问题的一般情况,请参阅danben 的回答。

TLDR 摘要:避免ifs 有助于矢量化。

因为 SIMD 过于复杂,无法在某些元素上进行分支,但在其他元素上不允许分支,if因此除非编译器知道可以将其重写为无分支操作集的“超优化”技术,否则任何包含语句的代码都将无法向量化。我不知道有任何编译器将其作为矢量化过程的一个组成部分(Clang 独立完成了其中的一些工作,但并非专门用于帮助矢量化 AFAIK)

使用 OP 提供的示例:

int a, b, low;
low = b + ((a - b) & ((a - b) >> 31));

许多编译器可以将其向量化为大致等价于:

__m128i low128i(__m128i a, __m128i b){
  __m128i diff, tmp;
  diff = _mm_sub_epi32(a,b);
  tmp = _mm_srai_epi32(diff, 31);
  tmp = _mm_and_si128(tmp,diff);
  return _mm_add_epi32(tmp,b);
}

这种优化需要以允许的方式布局数据,但它可以扩展到带有 avx2 的 __m256i 或带有 avx512 的 __m512i(甚至进一步展开循环以利用额外的寄存器)或其他 simd 指令其他架构。另一个优点是,这些指令都是低延迟、高吞吐量的指令(延迟约为 1,倒数吞吐量在 0.33 到 0.5 范围内 - 相对于非矢量化代码非常快)

我认为编译器没有理由不能将 if 语句优化为向量化条件移动(除了相应的 x86 操作仅适用于内存位置并且吞吐量低,而其他架构(如 arm)可能完全没有它)但它可以通过做类似的事情:

void lowhi128i(__m128i *a, __m128i *b){ // does both low and high
  __m128i _a=*a, _b=*b;
  __m128i lomask =  _mm_cmpgt_epi32(_a,_b),
  __m128i himask =  _mm_cmpgt_epi32(_b,_a);
  _mm_maskmoveu_si128(_b,lomask,a);
  _mm_maskmoveu_si128(_a,himask,b);
}

然而,由于内存读取和写入以及较低的吞吐量(更高/更差的互惠吞吐量),这将具有比上述示例高得多的延迟。

于 2018-03-13T00:07:41.987 回答
-1

除非您真的想降低效率,否则我认为这不是您需要担心的事情。

不过,我的简单想法是 if 会更快,因为它正在比较一件事,而其他代码正在执行多个操作。但同样,我想差异是微乎其微的。

于 2010-06-09T20:38:48.110 回答
-1

如果是 Gnu C++,试试这个

int min = i <? j;

我没有对它进行分析,但我认为它绝对是要击败的。

于 2010-06-09T23:03:10.040 回答