内联汇编中的子数组。 C++
Posted
技术标签:
【中文标题】内联汇编中的子数组。 C++【英文标题】:Subarrays in inline-assembly. C++ 【发布时间】:2020-04-30 07:49:49 【问题描述】:void new2d(int* aInit, int* aResult)
int cyclic[34] = 0;
for (int i = 0; i < 32; i++)
cyclic[i] = aInit[i];
cyclic[32] = aInit[0];
cyclic[33] = aInit[1];
float three = 3.0;
for (int i = 0; i < 32; i += 4)
int j = i + 1;
int k = j + 1;
__asm__ __volatile__
(
"vmovdqa (%0), %%xmm0;"
"vmovdqa (%1), %%xmm1;"
"vcvtdq2ps %%xmm0, %%xmm0;"
"vcvtdq2ps %%xmm1, %%xmm1;"
"addps %%xmm0, %%xmm1;"
"vmovdqa (%2), %%xmm1;"
"vcvtdq2ps %%xmm1, %%xmm1;"
"addps %%xmm0, %%xmm1;"
"vbroadcastss (%3), %%xmm1;"
"divps %%xmm0, %%xmm1;"
"vcvtps2dq %%xmm0, %%xmm0;"
"vmovdqa %%xmm0, (%4);"
:
: "a"(&(cyclic[i])), "b"(&(cyclic[j])), "c"(&(cyclic[k])), "d"(&three), "S"(&aResult[i])
);
尝试将一个初始数组的三个子数组相加,找到它们的均值,并将它们保存在结果数组中。虽然,在启动 .exe 文件后,它显示分段错误。 我该如何解决?使用 GNU 2.9.3,Ubuntu
【问题讨论】:
请注意,您的内联程序集无效,因为它没有声明适当的clobbers。使用内部函数可以更好地完成此类任务。 即使你安全地使用内联汇编,你也会因为许多错过的优化而自责,例如将3.0
的负载提升出循环,并乘以1.0/3
而不是使用慢除法。还为vcvtdq2ps
使用内存源操作数。如果您的整数不会溢出,请执行整数加法,而不是转换为 FP 并返回。此外,j
的负载 cyclic[i+1 + 0..3]
可以通过像 vpalignr
来自 cyclic[i]
向量和下一次迭代的 cyclic[i]
向量的随机播放来生成。未对齐的负载吞吐量通常非常好,所以可能不值得
如果你使用了内在函数,编译器可以为你做一些这样的事情,例如将vbroadcastss
(_mm_set1_ps(3.0f)
) 吊出循环。
【参考方案1】:
您正在对数组的未对齐元素使用vmovdqa
指令,该指令需要对齐的内存操作数。请改用vmovdqu
,用于加载和存储。或者更好的是,在实际计算指令中使用内存操作数(但这仅在 AVX 中有效;在传统 SSE 中,大多数指令的内存操作数必须对齐)。
汇编程序块还有其他低效率和问题。例如,正如 cmets 中所提到的,您缺少 clobbers,它指示可能由 asm 块修改的 CPU 和内存状态片段。在您的情况下,您缺少“memory”、“xmm0”和“xmm1”clobbers。如果没有这些,编译器将假定 asm 块不会影响内存内容(特别是 aResult
数组)或 xmm
寄存器(例如,将这些寄存器用于其自身目的与您的 asm 块冲突)。
此外,在某些情况下,您在覆盖或不使用先前指令的结果时,似乎弄乱了 addps
和 divps
指令中的输入和输出寄存器。在 gcc 使用的 AT&T x86 asm 语法中,最后一个操作数是输出操作数。在使用任何 AVX 指令时,您通常应该使用每条指令的 AVX 版本,尽管如果 YMM 寄存器的上半部分已经干净(例如 vzeroupper),将 128 位 AVX 指令与传统 SSE 混合不会导致 SSE/AVX 转换停止。使用 vaddps
/ vdivps
是可选的,但建议使用。
此外,您传递对输入和输出数组的引用效率低下。与其将指针传递给数组的特定元素,不如将内存引用传递给那些更有效,这允许编译器使用比普通指针更复杂的内存引用参数。这消除了在您的 asm 块之前在单独的指令中计算指针的需要。此外,在xmm
寄存器而不是内存中传递tree
常量更有效。理想情况下,您希望将 vbroadcastss
移出循环,但这只有在支持内在函数的情况下才有可能。 (或者在一个 asm 语句中编写循环。)
更正和改进的 asm 语句如下所示:
__asm__ __volatile__
(
"vcvtdq2ps %1, %%xmm0;"
"vcvtdq2ps %2, %%xmm1;"
"vaddps %%xmm1, %%xmm0;"
"vcvtdq2ps %3, %%xmm1;"
"vaddps %%xmm1, %%xmm0;"
"vbroadcastss %4, %%xmm1;"
"vdivps %%xmm0, %%xmm1;"
"vcvtps2dq %%xmm1, %0;"
: "=m"(aResult[i])
: "m"(cyclic[i]), "m"(cyclic[j]), "m"(cyclic[k]), "x"(three)
: "memory", "xmm0", "xmm1"
);
(实际上不必再是volatile
,因为内存输出是显式操作数。)
但更好的解决方案是使用内部函数来实现这个 asm 块。这不仅会使 asm 块更安全,而且会更高效,因为它会启用额外的编译器优化。当然,这只有在你的编译器支持内在函数时才有可能。
【讨论】:
是的,但这不会使内联汇编安全;它还缺少它使用的 XMM 寄存器上的"memory"
clobber 和 clobbers。 Buggy GNU C inline asm 编译并看起来可以工作是非常危险的;一个滴答作响的定时炸弹,等待破解周围的代码。编写有错误的代码也很容易(即很难正确),所以我认为重要的是我们不要让内联 asm 代码中的错误在 Stack Overflow 答案中未提及;这对于任何可能复制/粘贴一些代码作为示例而不阅读 SO commnets 的人来说都是危险的。
@PeterCordes 我已经扩展了我的答案。
"vbroadcastss %4, %%xmm1;"
内部循环内部仍然很糟糕,但至少它不是一个正确性错误。在不使用 Intel 内在函数的情况下,您可以像 xmmintrin.h
一样使用 GNU C 本机向量语法; typedef float v4f __attribute__((vector_size(16)))
这样您就可以将广播 3.0
作为寄存器操作数。顺便说一句,我认为您 不需要 需要 "memory"
clobber,因为您已将其转换为使用 "m"
输入/输出。我还会使用像[vi] "m"(cyclic[i])
这样的命名操作数,所以我可以在asm 模板中使用%vi
而不是%1
;容易混淆数字。
哦,"vcvtdq2ps %3, %%xmm1;"
是原始来源的错误;它会覆盖上一条指令的addps
结果。可能他们想要"vaddps %%xmm0, %%xmm1, %%xmm0 \n\t"
。还有其他类似的错误,类似于 AT&T 与 Intel 的语法混淆。
@PeterCordes 您确实需要“内存”破坏器,因为编译器不知道 aResult
数组的哪些部分被修改。关于内在函数和向量属性,我很谨慎,因为 OP 提到了 gcc 2.9.3(可能是 2.93?),这可能是古老的,我真的不知道它支持什么。以上是关于内联汇编中的子数组。 C++的主要内容,如果未能解决你的问题,请参考以下文章