整数上的无分支条件——速度快,但可以做得更快吗?

Posted

技术标签:

【中文标题】整数上的无分支条件——速度快,但可以做得更快吗?【英文标题】:Branchless conditionals on integers — fast, but can they be made faster? 【发布时间】:2015-11-01 01:52:50 【问题描述】:

我一直在尝试以下方法,并注意到此处定义的无分支“if”(现在用 &-!! 替换 *!!)可以在 64- 上将某些瓶颈代码加速(几乎)2 倍用叮当声位英特尔目标:

// Produces x if f is true, else 0 if f is false.
#define  BRANCHLESS_IF(f,x)          ((x) & -((typeof(x))!!(f)))

// Produces x if f is true, else y if f is false.
#define  BRANCHLESS_IF_ELSE(f,x,y)  (((x) & -((typeof(x))!!(f))) | \
                                     ((y) & -((typeof(y)) !(f))))

请注意,f 应该是一个没有副作用的相当简单的表达式,以便编译器能够进行最佳优化。

性能高度依赖于 CPU 和编译器。无分支的“if”性能在 clang 中表现出色;不过,我还没有发现无分支的“if/else”更快的情况。

我的问题是:这些写起来是否安全且可移植(意味着保证在所有目标上都给出正确的结果),并且它们可以做得更快吗?

无分支 if/else 的示例用法

这些计算 64 位最小值和最大值。

inline uint64_t uint64_min(uint64_t a, uint64_t b)

  return BRANCHLESS_IF_ELSE((a <= b), a, b);


inline uint64_t uint64_max(uint64_t a, uint64_t b)

  return BRANCHLESS_IF_ELSE((a >= b), a, b);

无分支 if 的示例用法

这是 64 位模加法 - 它计算 (a + b) % n。分支版本(未显示)遭受分支预测失败的严重影响,但无分支版本非常快(至少使用 clang)。

inline uint64_t uint64_add_mod(uint64_t a, uint64_t b, uint64_t n)

  assert(n > 1); assert(a < n); assert(b < n);

  uint64_t c = a + b - BRANCHLESS_IF((a >= n - b), n);

  assert(c < n);
  return c;

更新:无分支 if 的完整具体工作示例

下面是一个完整的 C11 程序,如果您想在您的系统上试用它,它演示了一个简单的 if 条件的分支版本和无分支版本之间的速度差异。该程序计算模幂,即(a ** b) % n,用于非常大的值。

要编译,请在命令行中使用以下命令:

-O3(或您喜欢的任何高优化级别) -DNDEBUG(为了速度,禁用断言) -DBRANCHLESS=0-DBRANCHLESS=1 分别指定分支或无分支行为

在我的系统上,会发生以下情况:

$ cc -DBRANCHLESS=0 -DNDEBUG -O3 -o powmod powmod.c && ./powmod
BRANCHLESS = 0
CPU time:  21.83 seconds
foo = 10585369126512366091

$ cc -DBRANCHLESS=1 -DNDEBUG -O3 -o powmod powmod.c && ./powmod
BRANCHLESS = 1
CPU time:  11.76 seconds
foo = 10585369126512366091

$ cc --version
Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.1.0
Thread model: posix

因此,在我的系统上,无分支版本的速度几乎是分支版本(3.4 GHz。Intel Core i7)的两倍。

// SPEED TEST OF MODULAR MULTIPLICATION WITH BRANCHLESS CONDITIONALS

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <time.h>
#include <assert.h>

typedef  uint64_t  uint64;

//------------------------------------------------------------------------------
#if BRANCHLESS
  // Actually branchless.
  #define  BRANCHLESS_IF(f,x)          ((x) & -((typeof(x))!!(f)))
  #define  BRANCHLESS_IF_ELSE(f,x,y)  (((x) & -((typeof(x))!!(f))) | \
                                       ((y) & -((typeof(y)) !(f))))
#else
  // Not actually branchless, but used for comparison.
  #define  BRANCHLESS_IF(f,x)          ((f)? (x) : 0)
  #define  BRANCHLESS_IF_ELSE(f,x,y)   ((f)? (x) : (y))
#endif

//------------------------------------------------------------------------------
// 64-bit modular multiplication.  Computes (a * b) % n without division.

static uint64 uint64_mul_mod(uint64 a, uint64 b, const uint64 n)

  assert(n > 1); assert(a < n); assert(b < n);

  if (a < b)  uint64 t = a; a = b; b = t;   // Ensure that b <= a.

  uint64 c = 0;
  for (; b != 0; b /= 2)
  
    // This computes c = (c + a) % n if (b & 1).
    c += BRANCHLESS_IF((b & 1), a - BRANCHLESS_IF((c >= n - a), n));
    assert(c < n);

    // This computes a = (a + a) % n.
    a += a - BRANCHLESS_IF((a >= n - a), n);
    assert(a < n);
  

  assert(c < n);
  return c;


//------------------------------------------------------------------------------
// 64-bit modular exponentiation.  Computes (a ** b) % n using modular
// multiplication.

static
uint64 uint64_pow_mod(uint64 a, uint64 b, const uint64 n)

  assert(n > 1); assert(a < n);

  uint64 c = 1;

  for (; b > 0; b /= 2)
  
    if (b & 1)
      c = uint64_mul_mod(c, a, n);

    a = uint64_mul_mod(a, a, n);
  

  assert(c < n);
  return c;


//------------------------------------------------------------------------------
int main(const int argc, const char *const argv[const])

  printf("BRANCHLESS = %d\n", BRANCHLESS);

  clock_t clock_start = clock();

  #define SHOW_RESULTS 0

  uint64 foo = 0;  // Used in forcing compiler not to throw away results.

  uint64 n = 3, a = 1, b = 1;
  const uint64 iterations = 1000000;
  for (uint64 iteration = 0; iteration < iterations; iteration++)
  
    uint64 c = uint64_pow_mod(a%n, b, n);

    if (SHOW_RESULTS)
    
      printf("(%"PRIu64" ** %"PRIu64") %% %"PRIu64" = %"PRIu64"\n",
             a%n, b, n, c);
    
    else
    
      foo ^= c;
    

    n = n * 3 + 1;
    a = a * 5 + 3;
    b = b * 7 + 5;
  

  clock_t clock_end = clock();
  double elapsed = (double)(clock_end - clock_start) / CLOCKS_PER_SEC;
  printf("CPU time:  %.2f seconds\n", elapsed);

  printf("foo = %"PRIu64"\n", foo);

  return 0;

第二次更新:Intel 与 ARM 的性能对比

在 32 位 ARM 目标(iPhone 3GS/4S、iPad 1/2/3/4,由 Xcode 6.1 用 clang 编译)上的测试表明,这里的无分支“if”实际上大约是 2-3 倍 在这些情况下,模幂代码比三元 ?:。因此,如果需要最大速度,这些无分支宏似乎不是一个好主意,尽管它们在需要恒定速度的极少数情况下可能很有用。 在 64 位 ARM 目标(iPhone 6+、iPad 5)上,无分支“if”的运行速度与三元 ?: 相同 — 同样由 Xcode 6.1 用 clang 编译。 对于 Intel 和 ARM(由 clang 编译),在计算最小值/最大值时,无分支“if/else”的速度大约是三元 ?: 的两倍。

【问题讨论】:

你是说这些比f ? a: b快? 另外值得注意的是,f 在第二个版本中被评估了两次,这可能会产生不良的副作用。 知道表达式(uint32_t)(x|(-x))&gt;&gt;31 等价于x==0? 0:1 可能会有所帮助。有关详细信息,请参阅here。 另一个技巧:#define MIN(a,b) (a &amp; (signed)((a-b)&gt;&gt;63)) | (b &amp; ~(signed)((a-b)&gt;&gt;63)). 我看到你讨厌输入 _t。 【参考方案1】:

当然这是可移植的,! 运算符保证会给出01 作为结果。然后将其提升为其他操作数所需的任何类型。

正如其他人所观察到的,您的 if-else 版本的缺点是要评估两次,但您已经知道这一点,如果没有副作用,您就可以了。

让我吃惊的是,你说这更快。我原以为现代编译器自己会执行这种优化。

编辑: 所以我用两个编译器(gcc 和 clang)和配置的两个值对此进行了测试。

事实上,如果你没有忘记设置-DNDEBUG=1,带有?:0 版本对于gcc 来说要好得多,并且可以满足我的预期。它基本上使用条件移动来使循环无分支。在那种情况下,clang 没有找到这种优化,而是进行了一些条件跳转。

对于带有算术的版本,gcc 的性能会变差。事实上,看到他这样做并不奇怪。它确实使用了imul 指令,而且这些指令很慢。铿锵声在这里更好。 “算术”实际上已经优化了乘法并用条件移动代替了它们。

总而言之,是的,这是可移植的,但如果这会带来性能改进或恶化,将取决于您的编译器、它的版本、您正在应用的编译标志、您的处理器的潜力......

【讨论】:

我也会这么认为。你使用什么编译器?我一直在使用 Clang。如果您想在自己的系统上试用,我刚刚更新了上面的内容,以包含一个完整的 C11 示例程序,该程序可以自行计时。 嘿,gcc 与 clang 的结果很酷,很有趣。是的,哇,这真的取决于编译器和目标 CPU。基准测试/分析在这里至关重要。就我而言,模乘是我代码的一部分中的一个瓶颈,所以我需要它尽可能快,所以 2 倍的加速是值得的......但这肯定不会总是这样。 顺便说一句,我重写了宏以用按位逻辑和 (&amp;-!!) 替换乘法 (*!!),现在 if 似乎运行相同的速度对我来说 gcc 是有分支的还是无分支的。

以上是关于整数上的无分支条件——速度快,但可以做得更快吗?的主要内容,如果未能解决你的问题,请参考以下文章

C语言速查手册:分支语句(Decision Making)

ImageMagick 是覆盖图像的最快方法吗?我怎样才能做得更快,或者有没有我不知道的更快的技术?

4. 整数规划:割平面法python代码

RandomSearchCV 超慢 - 故障排除性能增强

Python字典到排序的元组,这可以做得更好吗?

如何使用 GPU 获得更快的运行时间?