请考虑以下玩具示例,其中A
是按列大顺序存储的n x 2
矩阵,我想计算其列总和。 sum_0
仅计算第一列的总和,而sum_1
也计算第二列的总和。这实际上是一个人工示例,因为基本上不需要为此任务定义两个函数(我可以编写一个带有双循环嵌套的单个函数,其中外循环从0
迭代到j
) 。它旨在演示我实际遇到的模板问题。
/* "test.c" */
#include <stdlib.h>
// j can be 0 or 1
static inline void sum_template (size_t j, size_t n, double *A, double *c) {
if (n == 0) return;
size_t i;
double *a = A, *b = A + n;
double c0 = 0.0, c1 = 0.0;
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32)
for (i = 0; i < n; i++) {
c0 += a[i];
if (j > 0) c1 += b[i];
}
c[0] = c0;
if (j > 0) c[1] = c1;
}
#define macro_define_sum(FUN, j) \
void FUN (size_t n, double *A, double *c) { \
sum_template(j, n, A, c); \
}
macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)
如果我用
进行编译gcc -O2 -mavx test.c
GCC(例如最新的8.2)在进行内联,持续传播和消除无效代码后,将针对功能c1
(Check it on Godbolt)优化出涉及sum_0
的代码。
我喜欢这个把戏。通过编写单个模板函数并传入不同的配置参数,优化的编译器可以生成不同的版本。与复制和粘贴大部分代码并手动定义不同的功能版本相比,它要干净得多。
但是,如果我使用以下命令激活OpenMP 4.0+,则会失去这种便利性
gcc -O2 -mavx -fopenmp test.c
sum_template
不再被内联,并且不应用任何无效代码消除(Check it on Godbolt)。但是,如果我删除标志-mavx
以使用128位SIMD,则编译器优化将按我的期望进行工作(Check it on Godbolt)。那是一个错误吗?我正在使用x86-64(Sandybridge)。
备注
使用GCC的自动矢量化-ftree-vectorize -ffast-math
不会出现此问题(Check it on Godbolt)。但是我希望使用OpenMP,因为它允许跨不同的编译器进行可移植的对齐方式编译。
背景
我为R包编写模块,该模块需要跨平台和编译器移植。编写R扩展名不需要Makefile。当R在平台上构建时,它知道该平台上的默认编译器,并配置一组默认编译标志。 R没有自动矢量化标志,但是有OpenMP标志。这意味着使用OpenMP SIMD是在R包中利用SIMD的理想方法。有关更多说明,请参见1和2。
答案 0 :(得分:2)
我迫切需要解决此问题,因为在我的真实C项目中,如果没有模板技巧用于自动生成不同功能版本(以下简称为“版本转换”),那么我总共需要编写1400行9种不同版本的代码,而不是单个模板的200行代码。
我能够找到一条出路,现在使用问题中的玩具示例发布解决方案。
我计划使用 inline 函数sum_template
进行版本控制。如果成功,则在编译器执行优化时在编译时发生。但是,事实证明,OpenMP编译失败使此编译时版本控制失败。然后可以选择在预处理阶段仅使用 macros 进行版本控制。
要摆脱 inline 函数sum_template
,我将其手动内联到宏macro_define_sum
中:
#include <stdlib.h>
// j can be 0 or 1
#define macro_define_sum(FUN, j) \
void FUN (size_t n, double *A, double *c) { \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0; \
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1; \
}
macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)
在仅 macro 的此版本中,j
在宏扩展过程中直接由0或1代替。而在问题的 inline 函数+ macro 方法中,我在预处理阶段只有sum_template(0, n, a, b, c)
或sum_template(1, n, a, b, c)
,而{{1 j
正文中的}}仅在以后的编译时传播。
不幸的是,上述宏给出了错误。我无法在另一个宏中定义或测试宏(请参见1,2,3)。以sum_template
开头的OpenMP杂项在这里引起问题。因此,我必须将此模板分为两部分:在编译指示之前的部分和在编译指示之后的部分。
#
我不再需要#include <stdlib.h>
#define macro_before_pragma \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0;
#define macro_after_pragma(j) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1;
void sum_0 (size_t n, double *A, double *c) {
macro_before_pragma
#pragma omp simd reduction (+: c0) aligned (a: 32)
macro_after_pragma(0)
}
void sum_1 (size_t n, double *A, double *c) {
macro_before_pragma
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32)
macro_after_pragma(1)
}
。我可以使用定义的两个宏直接定义macro_define_sum
和sum_0
。我也可以适当地调整编译指示。在这里,我没有使用模板功能,而是具有用于功能代码块的模板,并且可以轻松地重用它们。
在这种情况下,编译器的输出是预期的(Check it on Godbolt)。
感谢各种反馈;它们都很有建设性(这就是为什么我喜欢Stack Overflow)。
感谢Marc Glisse,将我指向Using an openmp pragma inside #define。是的,没有搜索此问题对我来说是不好的。 sum_1
是指令,而不是真正的宏,因此必须有某种方法将其放入宏中。这是使用#pragma
运算符的简洁版本:
_Pragma
其他更改包括:
/* "neat.c" */
#include <stdlib.h>
// stringizing: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
#define str(s) #s
// j can be 0 or 1
#define macro_define_sum(j, alignment) \
void sum_ ## j (size_t n, double *A, double *c) { \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0; \
_Pragma(str(omp simd reduction (+: c0, c1) aligned (a, b: alignment))) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1; \
}
macro_define_sum(0, 32)
macro_define_sum(1, 32)
设为宏参数。对于AVX,值32表示对齐良好,而值8(alignment
)本质上表示没有对齐。需要Stringizing将这些标记解析为sizeof(double)
需要的字符串。使用_Pragma
检查预处理结果。编译会提供所需的程序集输出(Check it on Godbolt)。
使用编译器的函数属性。我不是专业的C程序员。我对C的经验仅来自编写R扩展。开发环境确定我对编译器属性不是很熟悉。我知道一些,但并不真正使用它们。
gcc -E neat.c
在我的应用程序中不是问题,因为我将分配对齐的内存并应用填充以确保对齐。我只需要向编译器承诺对齐方式,以便它可以生成对齐的加载/存储指令。我确实需要对未对齐的数据进行一些矢量化处理,但这仅占整个计算的非常有限的一部分。即使我因分割未对齐负载而导致性能下降,也不会在现实中注意到。我也不用自动矢量化编译每个C文件。我仅在L1缓存上的操作热时执行SIMD(即,它是CPU绑定而不是内存绑定)。顺便说一句,-mavx256-split-unaligned-load
用于 GCC ;对于其他编译器有什么作用?
我知道-mavx256-split-unaligned-load
和static inline
之间的区别。如果只有一个文件访问inline
函数,我将其声明为inline
,以便编译器不会生成它的副本。
即使没有 GCC 的static
,OpenMP SIMD也可以有效地进行还原。但是,在精简结束时,它不使用水平加法来累加累加器寄存器内的结果;它运行一个标量循环以将每个双字加起来(请参见Godbolt output中的代码块.L5和.L27)。
吞吐量是一个好点(特别是对于具有相对较大的延迟但具有高吞吐量的浮点运算)。应用SIMD的真正C代码是三重循环嵌套。我展开外部两个循环以扩大最内部循环中的代码块,以提高吞吐量。那么最里面的一个的矢量化就足够了。在这个“问题与解答”中的玩具示例中,我只是对一个数组求和,我可以使用-ffast-math
向 GCC 进行循环展开,并使用多个累加器来提高吞吐量。
我认为大多数人会以比我更多的技术方式来处理此问与答。他们可能对使用编译器属性或调整编译器标志/参数以强制函数内联感兴趣。因此,彼得的答案以及马克在该答案下的评论仍然非常有价值。再次感谢。
答案 1 :(得分:2)
解决此问题的最简单方法是使用__attribute__((always_inline))
或其他特定于编译器的替代。
#ifdef __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline)) inline
#elif defined(_MSC_VER)
#define ALWAYS_INLINE __forceinline inline
#else
#define ALWAYS_INLINE inline // cross your fingers
#endif
ALWAYS_INLINE
static inline void sum_template (size_t j, size_t n, double *A, double *c) {
...
}
此外,不要忘记使用-mtune=haswell
,而不仅仅是-mavx
。通常是个好主意。 (但是,有前途的对齐数据将阻止gcc的默认-mavx256-split-unaligned-load
调整将256位负载拆分为128位vmovupd
+ vinsertf128
,因此 this 的代码生成tune = haswell可以很好地使用该函数,但是通常情况下,您希望gcc可以自动矢量化其他函数。
您真的不需要static
和inline
一起使用;如果编译器决定不内联它,则它至少可以在编译单元之间共享相同的定义。
通常,gcc根据函数大小试探法决定是否内联。但是即使设置-finline-limit=90000
也不会使gcc与您的#pragma omp
(How do I force gcc to inline a function?)内联。我一直在猜测gcc并没有意识到内联后的常量传播会简化条件,但是90000个“伪指令”似乎很大。可能还有其他启发式方法。
可能,OpenMP对每个功能的设置方式不同,如果使优化器内联到其他功能中,可能会破坏优化器。使用__attribute__((target("avx")))
可以阻止该函数内联到不使用AVX编译的函数中(因此,您可以安全地进行运行时调度,而不必在整个if(avx)
条件下使用AVX指令来内联“感染”其他函数。)
OpenMP不能通过常规自动矢量化获得的一件事就是无需启用-ffast-math
就可以对减少量进行矢量化。
不幸的是,OpenMP仍然没有费心地使用多个累加器进行展开或隐藏FP延迟的任何操作。 #pragma omp
很好地暗示了一个循环实际上很热,值得花代码大小,因此即使没有-fprofile-use
,gcc也应该这样做。
因此,特别是如果它曾经在L2或L1高速缓存(或可能是L3)中很热的数据上运行,则应该做一些事情以获得更好的吞吐量。
顺便说一句,对齐对于Haswell的AVX通常不是什么大问题。但实际上,对于SKX上的AVX512,64字节对齐确实要重要得多。对于未对齐的数据,速度可能会降低20%,而不是几个百分点。
(但是在编译时承诺对齐与在运行时实际对齐数据是一个单独的问题。这都很有帮助,但是在gcc7及更早版本(或任何不带AVX的编译器)上,希望在编译时进行对齐会使代码更紧密。)< / p>