我试图找出如何构造数值模拟的主循环代码,以便编译器以紧凑的方式生成漂亮的矢量化指令 .
C伪代码最容易解释这个问题,但我也有一个受同一类问题影响的Fortran版本 . 考虑以下循环,其中 lots_of_code_*
是一些复杂的 expressions ,它产生了相当数量的机器指令 .
void process(const double *in_arr, double *out_arr, int len)
{
for (int i = 0; i < len; i++)
{
const double a = lots_of_code_a(i, in_arr);
const double b = lots_of_code_b(i, in_arr);
...
const double z = lots_of_code_z(i, in_arr);
out_arr[i] = final_expr(a, b, ..., z);
}
}
当使用AVX目标编译时,英特尔编译器会生成类似的代码
process:
AVX_loop
AVX_code_a
AVX_code_b
...
AVX_code_z
AVX_final_expr
...
SSE_loop
SSE_instructions
...
scalar_loop
scalar_instructions
...
生成的二进制文件已经相当大了 . 但是,我的实际计算循环看起来更像是以下内容:
void process(const double *in_arr1, ... , const double *in_arr30,
double *out_arr1, ... double *out_arr30,
int len)
{
for (int i = 0; i < len; i++)
{
const double a1 = lots_of_code_a(i, in_arr1);
...
const double a30 = lots_of_code_a(i, in_arr30);
const double b1 = lots_of_code_b(i, in_arr1);
...
const double b30 = lots_of_code_b(i, in_arr30);
...
...
const double z1 = lots_of_code_z(i, in_arr1);
...
const double z30 = lots_of_code_z(i, in_arr30);
out_arr1[i] = final_expr1(a1, ..., z1);
...
out_arr30[i] = final_expr30(a30, ..., z30);
}
}
这确实产生了非常大的二进制文件(Fortran版本为400KB,C99版本为800KB) . 如果我现在将 lots_of_code_*
定义为 functions ,则每个函数都会变为非向量化代码 . 每当编译器决定内联函数时,它都会对其进行矢量化,但似乎每次都会复制代码 .
在我看来,理想的代码应如下所示:
AVX_lots_of_code_a:
AVX_code_a
AVX_lots_of_code_b:
AVX_code_b
...
AVX_lots_of_code_z:
AVX_code_z
SSE_lots_of_code_a:
SSE_code_a
...
scalar_lots_of_code_a:
scalar_code_a
...
...
process:
AVX_loop
call AVX_lots_of_code_a
call AVX_lots_of_code_a
...
SSE_loop
call SSE_lots_of_code_a
call SSE_lots_of_code_a
...
scalar_loop
call scalar_lots_of_code_a
call scalar_lots_of_code_a
...
这显然会产生更小的代码,这仍然与完全内联版本一样优化 . 幸运的是,它甚至可能适合L1 .
显然我可以使用内在函数或其他任何东西来编写这个,但是有可能让编译器通过“普通”源代码以上述方式自动向量化吗?
我知道编译器可能永远不会为函数的每个矢量化版本生成单独的符号,但我认为它仍然只能在 process
内部内联每个函数并使用内部跳转来重复相同的代码块,而不是为每个输入复制代码阵列 .
3 回答
如果将lots_of_code块移动到没有for循环的单独编译单元中,它们可能不会进行修改 . 除非编译器具有矢量化的动机,否则它不会对代码进行矢量化,因为矢量化可能会导致管道中的延迟更长 . 为了解决这个问题,将循环拆分为30个循环,并将它们中的每一个放在一个单独的编译单元中:
像你这样的问题的正式答案:
Consider using OpenMP4.0 SIMD-enabled (I didn't say inlined) functions or equivalent proprietary mechanisms. Available in Intel Compiler or fresh GCC4.9.
点击此处查看更多详情:https://software.intel.com/en-us/node/522650
例:
它将使您能够使用函数调用来向量化循环而无需内联,因此无需生成大量代码 . (我没有详细探索你的代码片段;相反,我以文本形式回答了你提出的问题)
想到的直接问题是输入/输出指针上缺少
restrict
. 输入是const
,所以它可能不是太大的问题,除非你有多个输出指针 .除此之外,我推荐
-fassociative-math
或ICC等价物 . 在结构上,你似乎迭代数组,对数组进行多次独立操作,最后只在一起进行 . 严格的fp合规性可能会在阵列操作中杀死你 .最后,如果您需要比
vector_registers - input_arrays
更多的中间结果,则可能无法进行矢量化 .编辑:
我想我现在看到了你的问题 . 你在不同的数据上调用相同的函数,并希望每个结果独立存储,对吧?
问题是相同的函数总是写入相同的输出寄存器,因此后续的矢量化调用会破坏早期的结果 . 解决方案可能是:
一堆结果(在内存中或类似于旧的x87 FPU堆栈),每次都被推送 . 如果在内存中,它很慢,如果是x87,它就不会被矢量化 . 馊主意 .
有效地将多个函数写入不同的寄存器 . 代码重复 . 馊主意 .
旋转寄存器,就像在Itanium上一样 . 你不要孤单一人 .
它可以很容易地在当前架构上进行矢量化 . 抱歉 .
编辑,你在记忆中表现得很好:
这可能是可矢量化的 . 我不会那么快,以记忆速度,但你永远不会知道,直到你的基准 .