在 C 中的内联函数中循环展开

Loop unrolling in inlined functions in C

我有一个关于 C 编译器优化的问题,when/how 展开了内联函数中的循环。

我正在开发一个数字代码,它的功能类似于下面的示例。基本上,my_for() 会计算某种模板并调用 op() 为每个 imy_type *arg 中的数据做一些事情。在这里,my_func() 包装 my_for(),创建参数并将函数指针发送到 my_op()...谁的工作是为每个 ( arg->n) 双数组 arg->dest[j].

typedef struct my_type {
  int const n;
  double *dest[16];
  double const *src[16];
} my_type;

static inline void my_for( void (*op)(my_type *,int), my_type *arg, int N ) {
  int i;

  for( i=0; i<N; ++i )
    op( arg, i );
}

static inline void my_op( my_type *arg, int i ) {
  int j;
  int const n = arg->n;

  for( j=0; j<n; ++j )
    arg->dest[j][i] += arg->src[j][i];
}

void my_func( double *dest0, double *dest1, double const *src0, double const *src1, int N ) {
  my_type Arg = {
    .n = 2,
    .dest = { dest0, dest1 },
    .src = { src0, src1 }
  };

  my_for( &my_op, &Arg, N );
}

这很好用。这些函数按照它们应该的方式进行内联,并且代码(几乎)与在单个函数中内联编写所有内容并展开 j 循环一样高效,没有任何类型的 my_type Arg.

这里的困惑是:如果我在 my_op() 中设置 int const n = 2; 而不是 int const n = arg->n;,那么代码会变得和展开的单函数版本一样快。所以,问题是:为什么?如果所有内容都被内联到 my_func(),为什么编译器看不到我在字面上定义 Arg.n = 2?此外,当我明确地在 j 循环 arg->n 上进行绑定时,没有任何改进,它看起来应该像内联后更快的 int const n = 2; 一样。我还尝试在任何地方使用 my_type const 来真正向编译器发出这种 const-ness 信号,但它只是不想展开循环。

在我的数字代码中,这相当于大约 15% 的性能损失。如果重要的话,n=4 和这些 j 循环出现在 op().

中的几个条件分支中

我正在使用 icc (ICC) 12.1.5 20120612 进行编译。我尝试了 #pragma unroll。这是我的编译器选项(我错过了任何好的选项吗?):

-O3 -ipo -static -unroll-aggressive -fp-model precise -fp-model source -openmp -std=gnu99 -Wall -Wextra -Wno-unused -Winline -pedantic

谢谢!

速度更快,因为您的程序没有为变量分配内存。

如果您不必对未知值执行任何操作,它们将被视为 #define constant 2 并进行类型检查。它们只是在编译时添加的。

请您选择两个标签之一(我的意思是 C 或 C++),这很令人困惑,因为语言对待 const 值的方式不同 - C 将它们视为普通变量,而值不能改变了,在 C++ 中,它们根据上下文分配或不分配内存(如果您需要它们的地址,或者如果您需要在程序 运行 时计算它们,则分配内存)。

来源:"Thinking in C++"。没有确切的报价。

嗯,显然编译器 'smart' 不足以传播 n 常量和展开 for 循环。实际上它是安全的,因为 arg->n 可以在实例化和使用之间改变。

为了在各代编译器中获得一致的性能并最​​大限度地利用您的代码,请手动展开。

像我这样的人在这些情况下(性能为王)所做的就是依赖宏。

宏将 'inline' 在调试版本中(有用)并且可以使用宏参数进行模板化(到一定程度)。作为编译时常量的宏参数保证保持这种方式。