内联汇编中的子数组。 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 块冲突)。

此外,在某些情况下,您在覆盖或不使用先前指令的结果时,似乎弄乱了 addpsdivps 指令中的输入和输出寄存器。在 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++的主要内容,如果未能解决你的问题,请参考以下文章

纯 C++ 代码比内联汇编程序快 10 倍。为啥?

GCC内联汇编中的C数组?

C++ 多线程内联汇编

具有非内联汇编的 Qt C++ 项目

如何在 Visual C++ 6.0 中编写以下内联汇编代码?

GNU g++ 内联汇编块,如 Apple g++/Visual C++?