李哲源*_*李哲源 6 c gcc simd openmp auto-vectorization
考虑以下玩具示例,其中A是以n x 2列主要顺序存储的矩阵,我想计算其列总和.sum_0仅计算第1列的总和,同时sum_1也计算第2列的总和.这实际上是一个人为的例子,因为基本上不需要为这个任务定义两个函数(我可以编写一个带有双循环嵌套的函数,其中外循环从中迭代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)
Run Code Online (Sandbox Code Playgroud)
如果我用它编译它
gcc -O2 -mavx test.c
Run Code Online (Sandbox Code Playgroud)
在内联,常量传播和死代码消除之后,GCC(比如最新的8.2)将优化涉及c1函数的代码sum_0(在Godbolt上检查它).
我喜欢这个技巧.通过编写单个模板函数并传入不同的配置参数,优化编译器可以生成不同的版本.它比复制和粘贴大部分代码并手动定义不同的功能版本要清晰得多.
但是,如果我激活OpenMP 4.0+,这种便利性就会丢失
gcc -O2 -mavx -fopenmp test.c
Run Code Online (Sandbox Code Playgroud)
sum_template不再内联,也不应用死代码消除(在Godbolt上查看).但是如果我删除标志-mavx以使用128位SIMD,编译器优化就像我期望的那样工作(在Godbolt上检查它).这是一个错误吗?我在x86-64(Sandybridge).
备注
使用GCC的自动矢量化-ftree-vectorize -ffast-math不会出现这个问题(在Godbolt上查看).但我希望使用OpenMP,因为它允许跨不同编译器的可移植对齐pragma.
背景
我为R包编写模块,需要跨平台和编译器移植.编写R扩展名不需要Makefile.当R构建在平台上时,它知道该平台上的默认编译器是什么,并配置一组默认编译标志.R没有自动矢量化标志,但它有OpenMP标志.这意味着使用OpenMP SIMD是在R包中使用SIMD的理想方式.有关详细说明,请参见1和2.
我迫切需要解决这个问题,因为在我真正的C项目中,如果没有使用模板技巧来自动生成不同的函数版本(以下简称“版本控制”),我总共需要编写1400行代码9 个不同的版本,而不是单个模板只有 200 行。
我找到了出路,现在使用问题中的玩具示例发布解决方案。
我计划利用内联函数sum_template进行版本控制。如果成功,它会在编译器执行优化时发生。然而,OpenMP pragma 结果导致编译时版本控制失败。然后,可以选择仅使用宏在预处理阶段进行版本控制。
为了摆脱内联函数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)
Run Code Online (Sandbox Code Playgroud)
在此仅包含宏的版本中,j在宏扩展期间直接用 0 或 1 替换。而在问题中的内联函数+宏方法中,我只有sum_template(0, n, a, b, c)orsum_template(1, n, a, b, c)在预处理阶段,并且j在 的主体中sum_template仅在稍后的编译时传播。
不幸的是,上面的宏给出了错误。我无法在另一个宏中定义或测试宏(参见1、2、3)。以 开头的 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)
}
Run Code Online (Sandbox Code Playgroud)
我不再需要了macro_define_sum。我可以直接使用定义的两个宏来定义sum_0和直接使用。sum_1我还可以适当调整pragma。在这里,我没有模板函数,而是函数代码块的模板,并且可以轻松地重用它们。
在这种情况下,编译器输出符合预期(在 Godbolt 上检查)。
感谢您的各种反馈;他们都非常有建设性(这就是我喜欢 Stack Overflow 的原因)。
感谢Marc Glisse指出在 #define 中使用 openmp pragma。是的,我很遗憾没有搜索这个问题。#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)
Run Code Online (Sandbox Code Playgroud)
其他变化包括:
alignment进行了宏观论证。对于 AVX,值 32 意味着良好对齐,而值 8 ( sizeof(double)) 基本上意味着没有对齐。需要字符串化_Pragma来将这些标记解析为需要的字符串。用于gcc -E neat.c检查预处理结果。编译给出了所需的汇编输出(在 Godbolt 上检查)。
使用编译器的函数属性。我不是专业的 C 程序员。我使用 C 的经验仅仅来自于编写 R 扩展。开发环境决定了我对编译器属性不是很熟悉。我知道一些,但并没有真正使用它们。
-mavx256-split-unaligned-load在我的应用程序中这不是问题,因为我将分配对齐的内存并应用填充以确保对齐。我只需要向编译器保证对齐,以便它可以生成对齐的加载/存储指令。我确实需要对未对齐的数据进行一些矢量化,但这对整个计算的影响非常有限。即使我因分割未对齐负载而受到性能损失,但实际上也不会被注意到。我也不使用自动矢量化来编译每个 C 文件。我仅在 L1 缓存上的操作很热时才执行 SIMD(即,它受 CPU 限制而不是内存限制)。顺便说一句,-mavx256-split-unaligned-load是针对GCC的;对于其他编译器来说是什么?
我知道static inline和之间的区别inline。如果一个inline函数仅由一个文件访问,我会将其声明为,static以便编译器不会生成它的副本。
即使没有GCC,OpenMP SIMD 也可以有效地进行缩减-ffast-math。然而,它在归约结束时不使用水平加法来聚合累加器寄存器内的结果;它运行一个标量循环来将每个双字相加(请参阅Godbolt 输出中的代码块 .L5 和 .L27 )。
吞吐量是一个好点(特别是对于延迟较大但吞吐量较高的浮点运算)。我真正应用 SIMD 的 C 代码是三重循环嵌套。我展开外部两个循环以扩大最内部循环中的代码块以提高吞吐量。最里面的向量化就足够了。在本问答中的玩具示例中,我只是对一个数组求和,我可以-funroll-loops要求GCC进行循环展开,使用多个累加器来提高吞吐量。
我想大多数人都会比我以更技术性的方式来对待这个问答。他们可能对使用编译器属性或调整编译器标志/参数以强制函数内联感兴趣。因此,Peter的回答以及Marc在答案下的评论仍然非常有价值。再次感谢。
| 归档时间: |
|
| 查看次数: |
2729 次 |
| 最近记录: |