【问题标题】:Compiler optimization of if statement in CC语言中if语句的编译器优化
【发布时间】:2017-10-06 23:06:04
【问题描述】:

我在 C 中有一个这样的函数(在伪代码中,删除了不重要的部分):

int func(int s, int x, int* a, int* r) {
    int i;

    // do some stuff

    for (i=0;i<a_really_big_int;++i) {
        if (s) r[i] = x ^ i;
        else r[i] = x ^ a[i];
        // and maybe a couple other ways of computing r
        // that are equally fast individually
    }

    // do some other stuff

}

这段代码被如此频繁地调用,以至于这个循环实际上是代码中的一个速度瓶颈。我想知道几件事:

  1. 由于开关 s 是函数中的常量,优秀的编译器会优化循环以使分支不会一直拖慢速度吗?

  2. 如果没有,有什么好的方法可以优化这段代码?

====

这里有一个更完整示例的更新:

int func(int s,
         int start,int stop,int stride,
         double *x,double *b,
         int *a,int *flips,int *signs,int i_max,
         double *c)
{
  int i,k,st;
  for (k=start; k<stop; k += stride) {
    b[k] = 0;
    for (i=0;i<i_max;++i) {

      /* this is the code in question */
      if (s) st = k^flips[i];
      else st = a[k]^flips[i];
      /* done with code in question */

      b[k] += x[st] * (__builtin_popcount(st & signs[i])%2 ? -c[i] : c[i]);
    }
  }
}

编辑 2:

如果有人好奇,我最终重构了代码并将整个内部 for 循环(使用 i_max)提升到外部,使 really_big_int 循环更简单,并且希望易于矢量化! (并且还避免了无数次做一堆额外的逻辑)

【问题讨论】:

  • 如果可能,执行两个if 语句,每个语句都包含一个循环,而不是包含两个if 语句的循环。然后你只检查一次条件,而不是a_really_big_int 次。这取决于您当前循环中的其他内容是否以这种方式“可提取”。
  • @hnefatl 对——我想避免这种情况的原因是因为实际循环是几个嵌套循环,其中包含一些不完全不重要的逻辑,我不想重复码了很多次。但也许这是最好的解决方案
  • @Barmar 给出的示例变得可以简单地矢量化,因此gcc 绝对if() 提升到循环之外。
  • @EOF 但也许他的实际代码与给出的示例并不完全相同。他说他遗漏了“不重要”的部分。它们可能对 GCC 优化器很重要。
  • @G.Meyer 你可以让所有变量都成为我关心的函数参数。唯一的一点是,我可以将它扔给我的编译器并获得其他人都可以验证的结果(不像 必须对你的代码进行钢锯来测试它)。

标签: c optimization compiler-optimization


【解决方案1】:

优化代码的一个明显方法是将条件拉到循环之外:

if (s)
    for (i=0;i<a_really_big_int;++i) {
        r[i] = x ^ i;
    }
else
    for (i=0;i<a_really_big_int;++i) {
        r[i] = x ^ a[i];
    }

精明的编译器可能能够将其更改为一次多个元素的 r[] 赋值。

【讨论】:

  • “精明的编译器”不需要帮助,它可以提升 if() 就好了。
  • @EOF:你错过了我的意思。我说的是if 被吊起后会发生什么。
  • ... 现在它可以使用_mm*_xor_si*()_mm*_set_epi32() 和未对齐的加载/存储进行矢量化(除非您可以确保输入对齐),但希望编译器可以自己完成
【解决方案2】:

微优化

通常他们不值得花时间 - 审查更大的问题更有效。

还需进行微优化,尝试各种方法,然后对其进行分析以找到最佳方法,这可以带来适度的改进。

除了@wallyk@kabanus 好的答案之外,一些简单的编译器还受益于以0 结尾的循环。

// for (i=0;i<a_really_big_int;++i) {
for (i=a_really_big_int; --i; ) {

[编辑第二次优化]

OP 添加了一个更具竞争力的示例。问题之一是编译器无法假设b 指向的内存与其他内存不重叠。这会阻止某些优化。

假设它们实际上不重叠,请在 b 上使用 restrict 以允许优化。 const 也有助于较弱的编译器不会推断出这一点。如果参考数据不重叠,其他人的restrict 也可能受益。

// int func(int s, int start, int stop, int stride, double *x,
//     double *b, int *a, int *flips,
//     int *signs, int i_max, double *c) {

int func(int s, int start, int stop, int stride, const double * restrict x,
    double * restrict b, const int * restrict a, const int * restrict flips, 
    const int * restrict signs, int i_max, double *c) {

【讨论】:

    【解决方案3】:

    您的所有命令都是循环中的快速 O(1) 命令。 if 绝对是优化的,如果你的所有命令都是r[i]=somethingquick 的形式,你的 for+if 也是如此。这个问题可能会归结为 big int 有多小?

    一个快速的int main 只是从INT_MININT_MAX 求和成一个长变量,在 Windows 上的 Ubuntu 子系统上对我来说大约需要 10 秒。您的命令可能会将其乘以几,很快就会达到一分钟。归根结底,如果您真的要进行大量迭代,这可能是无法避免的。

    如果r[i] 是独立计算的,这将是线程/多处理的经典用法。

    编辑:

    我认为% 无论如何都被编译器优化了,但如果没有,请注意x &amp; 1 对于奇数/偶数检查要快得多。

    【讨论】:

    • re:内置,我认为取决于您的体系结构 __builtin_popcount 无论如何都是一条机器指令(来自 gcc)。不过我不确定。无论如何,我真的怀疑 int 的汉明权重需要花费大量时间来计算......
    • @G.Meyer 它是(返回 1 或其他东西)。虽然不确定这是一条指令,但谢谢。
    【解决方案4】:

    假设 x86_64,可以确保指针对齐到 16 个字节并使用intrinsics。如果它仅在带有 AVX2 的系统上运行,您可以使用 __mm256 变体(类似于 avx512*)

    int func(int s, int x, const __m128i* restrict a, __m128i* restrict r) {
        size_t i = 0, max = a_really_big_int / 4;
        __m128i xv =  _mm_set1_epi32(x);
        // do some stuff
        if (s) {
            __m128i iv = _mm_set_epi32(3,2,1,0); //or is it 0,1,2,3?
            __m128i four = _mm_set1_epi32(4);
            for ( ;i<max; ++i, iv=_mm_add_epi32(iv,four)) {
                r[i] = _mm_xor_si128(xv,iv);
            }
        }else{ /*not (s)*/
            for (;i<max;++i){
                r[i] = _mm_xor_si128(xv,a[i]);
            }
        }
        // do some other stuff   
    }
    

    【讨论】:

      【解决方案5】:

      虽然if 语句将在任何体面的编译器上被优化掉(除非你要求编译器不要优化),但我会考虑将优化写入其中(以防你在没有优化的情况下编译)。

      此外,虽然编译器可能会优化“绝对”if 语句,但我会考虑手动优化它,使用任何可用的内置函数或 using bitwise 操作。

      b[k] += x[st] *
              ( ((__builtin_popcount(st & signs[I]) & 1) *
                 ((int)0xFFFFFFFFFFFFFFFF)) ^c[I] );
      

      这将取popcount 的最后一位(1 == 奇数,0 == 偶数),将其乘以 const(如果为奇数,则所有位为 1,如果为真,则所有位为 0)然后对c[I] 进行异或运算值(与0-c[I]~(c[I]) 相同。

      这将避免在第二个absolute if 语句未优化的情况下发生指令跳转。

      附言

      我使用了一个 8 字节长的值,并通过将其转换为 int 来截断它的长度。这是因为我不知道int 在您的系统上可能存在多长时间(在我的系统上是 4 个字节,即0xFFFFFFFF)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-12-27
        • 1970-01-01
        • 2017-06-16
        • 1970-01-01
        • 2013-05-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多