【问题标题】:How to convert this assembly code to intrinsic code?如何将此汇编代码转换为内部代码?
【发布时间】:2019-11-29 23:44:49
【问题描述】:

下面看起来像是内在函数,但是,我不熟悉内在函数。请帮我转换真实的代码。特别是, testFunc() 对我来说更加模棱两可。 我想这也是两个浮点向量的点积,但是标签 Lrep 和 Lexit 让我感到困惑。 请帮我弄清楚。 并且内在函数可用于移动处理器?

void testFunc(int M, int N, int K, float* A, float* B, float* C)
{
    float *a;
    float *b = new float[K*N];
    float *pointb = B;
    float *bb;
    float *answer = C;
    float c[8];

    for (int j = 0, k; j < K; j++) {
        bb = b + j;
        for (k = N / 8; k > 0; k--) {
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
            *bb = *pointb++; bb += K;
        }
        for (k = N / 8 * 8; k < N; k++) {
            *bb = *pointb++; bb += K;
        }
    }

    int K8 = K / 8 * 8;

    for (int i = 0; i < M; i++) for (int k = 0; k < N; k++) {
        a = A + i * K;
        bb = b + k * K;
        __asm {
            mov             esi, K8;
            sub             esi, 8;
            shl             esi, 2;
            xor             edi, edi;
            mov             edx, a;
            mov             ebx, bb;
            vxorps          ymm3, ymm3, ymm3;
        Lrep:
            cmp             edi, esi;
            jg              Lexit;
            vmovups         ymm0, ymmword ptr[edx + edi];
            vfmadd231ps     ymm3, ymm0, ymmword ptr[ebx + edi];
            add             edi, 32;
            jmp             Lrep;
        Lexit:
            vmovups         ymmword ptr[c], ymm3;
        }

        for (int j = K8; j < K; ) {
            *c += *(a + j) * *(bb + j); j++;
        }

        *answer = (c[0] + c[1] + c[2] + c[3] + c[4] + c[5] + c[6] + c[7]);
        answer++;
    }
}

pA = A;
for (k = 0; k < K; k++) {
    pC = C;
    for (i = 0; i < M; i++) {
        pA = A + i * K + k;
        pB = B + k * N;
        for (j = N / 32; j > 0; j--) {
            _asm {
                mov             eax, pC;
                mov             ebx, pA;
                mov             ecx, pB;
                vmovups         ymm0, ymmword ptr[eax];
                vmovss          xmm1, dword ptr[ebx];
                vbroadcastss    ymm4, xmm1;
                vmovups         ymm2, ymmword ptr[ecx];
                vfmadd231ps     ymm0, ymm4, ymm2;
                vmovups         ymmword ptr[eax], ymm0;
            }
            pC += 8; pB += 8;
            _asm {
                mov             eax, pC;
                mov             ebx, pA;
                mov             ecx, pB;
                vmovups         ymm0, ymmword ptr[eax];
                vmovss          xmm1, dword ptr[ebx];
                vbroadcastss    ymm4, xmm1;
                vmovups         ymm2, ymmword ptr[ecx];
                vfmadd231ps     ymm0, ymm4, ymm2;
                vmovups         ymmword ptr[eax], ymm0;
            }
            pC += 8; pB += 8;
            _asm {
                mov             eax, pC;
                mov             ebx, pA;
                mov             ecx, pB;
                vmovups         ymm0, ymmword ptr[eax];
                vmovss          xmm1, dword ptr[ebx];
                vbroadcastss    ymm4, xmm1;
                vmovups         ymm2, ymmword ptr[ecx];
                vfmadd231ps     ymm0, ymm4, ymm2;
                vmovups         ymmword ptr[eax], ymm0;
            }
            pC += 8; pB += 8;
            _asm {
                mov             eax, pC;
                mov             ebx, pA;
                mov             ecx, pB;
                vmovups         ymm0, ymmword ptr[eax];
                vmovss          xmm1, dword ptr[ebx];
                vbroadcastss    ymm4, xmm1;
                vmovups         ymm2, ymmword ptr[ecx];
                vfmadd231ps     ymm0, ymm4, ymm2;
                vmovups         ymmword ptr[eax], ymm0;
            }
            pC += 8; pB += 8;
        }
        for (j = N / 32 * 32; j < N; j++) {
            *pC += *pA * *pB;
            pC += 1; pB += 1;
        }
    }
}

【问题讨论】:

  • 您可以直接将汇编代码复制到__asm 块中。但是,您的项目架构应该是 x86,因为不支持 x64。
  • 嗨,seccpur。谢谢您的回答。我已经在我的代码中复制了汇编代码。我的麻烦不在于无法使用汇编代码,而是无法正确调试此内联汇编代码。 VS 2015 调试器跳过这些 asm 行,因此它指向不正确的行。

标签: c assembly simd intrinsics avx


【解决方案1】:

2 个向量负载(来自 2 个数组中的同一位置)将 FMA 馈送到向量累加器中,这对我来说闻起来像点积。

我没有查看 asm 参考手册来确定目标操作数是和而不是被乘数的 1,但这是有意义的方式。

三重嵌套循环看起来像矩阵乘法。它广播 1 个输入,同时从另一个输入进行向量加载以提供 FMA,因此它可能正在为输出行生成结果的 SIMD 向量。

为此使用 MSVC 内联 asm 语法非常糟糕;它只能通过内存操作数接受输入,因此它强制在每个 asm 块之间重新加载 + 存储。如果要展开,请使用一个大的 asm 语句并在寻址模式中使用位移。


IDK 为什么点生成循环的编写效率低下(循环内有条件分支和无条件分支),并且没有使用多个累加器展开。几乎违背了在 asm 中手动编码的目的。请参阅Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?,了解如何使用多个累加器来隐藏 FMA 延迟。或者在展开+矢量化纯 C 循环时让 clang 为您完成。

我也不知道为什么它不对结果进行水平求和,而是使用vmovups [c], ymm3 将其存储到内存中。似乎毫无意义。我猜调用者必须从内存中重新加载并求和,或者您可以将函数声明为返回 __m256 向量并忽略存储。


无论如何,您显然可以在标量 C 代码中编写点积,也许使用 math.h 中的fma(a[i], b[i], sum) 来复制 asm 不舍入临时结果的行为。

或者使用 sum = _mm256_fmadd_ps(_mm256_loadu_ps(a[i]), _mm256_loadu_ps(b[i]), sum); 之类的内在函数复制手动矢量化。 (见Intel's intrinsics guide)。

【讨论】:

    【解决方案2】:

    在内部函数中,这段代码重复了 4 次。

    {
    // vmovups         ymm0, ymmword ptr[eax];
    __m256 tempC = _mm256_loadu_ps((float*)pC);
    
    // vmovss          xmm1, dword ptr[ebx];
    // vbroadcastss    ymm4, xmm1;
    __m256 tempA = _mm256_set1_ps(*pA);
    
    // vmovups         ymm2, ymmword ptr[ecx];
    __m256 tempB = _mm256_loadu_ps((float*)pB);
    
    // vfmadd231ps     ymm0, ymm4, ymm2;
    __m256 result = _mm256_fmadd_ps(tempA, tempB, tempC);
    
    // vmovups         ymmword ptr[eax], ymm0;
    _mm256_storeu_ps(pC, result);
    }
    
    pC += 8; pB += 8;
    

    不过,不断地从 pA 广播相同的值似乎有点多余。

    【讨论】:

    • 感谢您的友好回答。你能推荐一些学习内在函数的材料吗?
    • @Seyon 不是。这是一种资源很快就会过时的事情。我只是利用英特尔内在函数指南:software.intel.com/sites/landingpage/IntrinsicsGuide/… 和 Godbolt(带有 -mavx2 -mfma -O2 和 -ffast-maths 标志)。
    • @robthebloke 和未来的读者:更喜欢-march=haswell-march=znver1 而不是-mavx2 -mfma。尤其是使用 gcc,将 -mtune= 设置为具有 AVX2 + FMA 的 Intel CPU 会排除将 loadu / storeu 拆分为多个 asm 指令的可能性。 Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd? 如果您的数据在运行时实际上是对齐的,这可能是一个很大的缺点。
    • 使用内在函数而不是 asm 的一个优点是编译器可以将广播提升到循环之外。 (至少如果您使用float *restrict pCpA,那么编译器知道通过pC 进行的存储不会影响从pA 读取的值。)是的,当我在写下我的答案,但我不能把手指放在上面。冗余负载解释了为什么快速浏览并不能清楚地表明它是循环一列还是跨行。很好发现。
    【解决方案3】:

    我会做前几行来帮助您入门,但实际上,如果您无法阅读程序集,则需要参考 Intel CPU 手册才能对其进行解读。

    mov             esi, K8;
    sub             esi, 8;
    shl             esi, 2;
    xor             edi, edi;
    mov             edx, a;
    mov             ebx, bb;
    mov             esi, K8
    
    1. 将K8的内容复制到esi中
    2. easi 中的值减去 8
    3. esi左移2位,复制结果到esi中
    4. 针对 edi 对 edi 应用异或运算(如果您了解二进制和寄存器的工作原理,这将为 0,原因很清楚)
    5. 将 a 的内容复制到 edx 中
    6. 将bb的内容复制到ebx中
    7. 将K8的内容复制到esi中

    从这里开始,您需要熟悉与您的问题相关的二进制和基本 cpu 架构以及汇编语言操作数,具体取决于您的知识所在。一旦你能读懂每一行,你就可以破译块,最后是程序。

    【讨论】:

    • 感谢您的友好回答。那么“vxorps ymm3, ymm3, ymm3;”呢?我应该了解更多关于这方面的信息?
    • 尽管在快速谷歌搜索之前我还没有遇到 vxorps 指令,但我只需要找到定义。我也是 SO 的新手,但我很确定我的想法不仅仅是为您回答所有问题,而是将您推向正确的方向。如果我错了,请纠正我。
    猜你喜欢
    • 1970-01-01
    • 2019-11-30
    • 1970-01-01
    • 2020-11-04
    • 2013-01-22
    • 1970-01-01
    • 2021-08-16
    • 1970-01-01
    • 2014-09-01
    相关资源
    最近更新 更多