我急需解决这个问题,因为在我的真实C项目中,如果没有使用模板技巧进行不同函数版本的自动生成(以下简称“版本控制”),我将需要编写9个不同版本的总共1400行代码,而不仅仅是单个模板的200行代码。
我找到了一种解决方法,并在此发布一个使用问题中的玩具示例的解决方案。
我计划利用一个内联函数sum_template
进行版本控制。如果成功,它会在编译器执行优化时发生。然而,OpenMP pragma无法通过这种编译时版本控制。选项是仅使用宏在预处理阶段进行版本控制。
为了摆脱内联函数sum_template
,我手动将其内联到宏macro_define_sum
中:
#include <stdlib.h>
#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)
在这个仅包含宏的版本中,
j
在宏扩展期间直接被替换为0或1。而在问题中的内联函数+宏方法中,我只有
sum_template(0, n, a, b, c)
或
sum_template(1, n, a, b, c)
在预处理阶段,
sum_template
中的
j
仅在后续编译时传播。
不幸的是,上述宏会导致错误。我不能在另一个宏中定义或测试宏(请参见
1,
2,
3)。以
#
开头的OpenMP指示在这里引起了问题。因此,我必须将此模板分成两个部分:pragma之前的部分和之后的部分。
#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
和sum_1
。我也可以适当地调整pragma。这里我没有模板函数,而是有函数代码块的模板,并且可以轻松地重用它们。
在这种情况下,编译器输出如预期的那样(在Godbolt上检查)。
更新
感谢各种反馈;它们都非常有建设性(这就是为什么我喜欢Stack Overflow)。
感谢Marc Glisse指出了在#define内使用openmp pragma。是的,我犯了错误,没有搜索这个问题。#pragma是一个指令,而不是真正的宏,因此必须有一些方法将其放入宏中。这里是使用_Pragma运算符的整洁版本:
#include <stdlib.h>
#define str(s) #s
#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)
其他更改包括:
- 我使用标记连接生成函数名称;
alignment
成为宏参数。对于AVX,32的值表示良好的对齐,而8(sizeof(double)
)的值基本上表示没有对齐。字符串化需要将这些标记解析为_Pragma
所需的字符串。
使用gcc -E neat.c
检查预处理结果。编译会产生所需的汇编输出(在Godbolt上检查)。
对Peter Cordes详细答案的一些评论
使用编译器的函数属性。我不是专业的C程序员。我的C经验仅来自编写R扩展。开发环境决定了我对编译器属性不太熟悉。我知道一些,但实际上并不使用它们。
-mavx256-split-unaligned-load
在我的应用程序中不是问题,因为我将分配对齐内存并应用填充以确保对齐。我只需要承诺编译器对齐,以便它可以生成对齐的加载/存储指令。我确实需要在非对齐数据上进行一些矢量化,但这只占整个计算的很小一部分。即使我在分裂非对齐负载上受到性能惩罚,现实中也不会注意到。我也不会用自动向量化来编译每个C文件。我只在操作在L1缓存上很热时(即它是CPU绑定而不是内存绑定)使用SIMD。顺便说一下,-mavx256-split-unaligned-load
是为GCC设计的;其他编译器呢?
我知道static inline
和inline
之间的区别。如果一个inline
函数只被一个文件访问,我会将其声明为static
,这样编译器就不会生成它的副本。
OpenMP SIMD可以有效地执行归约,即使没有GCC的-ffast-math
。然而,它不使用水平加法来在累加器寄存器中聚合结果,而是运行标量循环来相加每个双字(请参见代码块.L5和.L27在Godbolt输出中)。
吞吐量是一个很好的优点(特别是对于具有相对较大延迟但高吞吐量的浮点运算)。我应用了SIMD的真实C代码是三重循环嵌套。我展开外部两个循环,以增大最内层循环中的代码块,以增强吞吐量。然后对最内层进行向量化即可。在这个问题和答案中的玩具示例中,我可以使用
-funroll-loops
来请求
GCC展开循环,并使用多个累加器来增强吞吐量。
关于这个问题和答案
我认为大多数人会比我更以技术的方式看待这个问题和答案。他们可能对使用编译器属性或调整编译器标志/参数来强制函数内联感兴趣。因此,Peter的答案以及Marc在答案下的评论仍然非常有价值。再次感谢。
-ffunction-sections,-fdata-sections
编译器选项和-Wl,--gc-sections
链接器选项实现的(Apple链接器使用-Wl,-dead_code
)。 - jww