【问题标题】:Uint8 to mm0 registerUint8 到 mm0 寄存器
【发布时间】:2020-12-27 21:22:00
【问题描述】:

我一直在玩 this 演示文稿中的示例(幻灯片 41)。

就我而言,它执行 alpha 混合。

MOVQ mm0, alpha//4 16-b zero-padding α
MOVD mm1, A //move 4 pixels of image A 
MOVD mm2, B //move 4 pixels of image B
PXOR mm3 mm3 //clear mm3 to all zeroes 
//unpack 4 pixels to 4 words
PUNPCKLBW mm1, mm3 // Because B -A could be
PUNPCKLBW mm2, mm3 // negative, need 16 bits
PSUBW mm1, mm2 //(B-A) 
PMULHW mm1, mm0 //(B-A)*fade/256 
PADDW mm1, mm2 //(B-A)*fade + B 
//pack four words back to four bytes
PACKUSWB mm1, mm3

我想用汇编程序用c重写它。

现在,我有这样的事情:

void fade_mmx(SDL_Surface* im1,SDL_Surface* im2,Uint8 alpha, SDL_Surface* imOut)
{
    int pixelsCount = imOut->w * im1->h;
    
    Uint32 *A = (Uint32*) im1->pixels;
    Uint32 *B = (Uint32*) im2->pixels;
    Uint32 *out = (Uint32*) imOut->pixels;
    Uint32 *end = out + pixelsCount;

    __asm__ __volatile__ (
            "\n\t movd  (%0), %%mm0"
            "\n\t movd  (%1), %%mm1"
            "\n\t movd  (%2), %%mm2"
            "\n\t pxor       %%mm3, %%mm3"
            "\n\t punpcklbw  %%mm3, %%mm1"
            "\n\t punpcklbw  %%mm3, %%mm2"
            "\n\t psubw      %%mm2, %%mm1"
            "\n\t pmulhw     %%mm0, %%mm1"
            "\n\t paddw      %%mm2, %%mm1"
            "\n\t packuswb   %%mm3, %%mm1"
    : : "r" (alpha), "r" (A), "r" (B), "r" (out), "r" (end)
    );
    __asm__("emms" : : );
}

编译时我收到以下消息:Error: (%dl) is not a valid base/index expression 关于汇编程序中的第一行。 我怀疑这是因为alphaUint8,我尝试转换它,但后来出现分段错误。在示例中,他们谈论的是4 16-b zero-padding α,这对我来说并不是很清楚。

【问题讨论】:

  • alpha 转换为uint32 并删除(%0) 中的括号。
  • 我会尝试Uint16 alphaArray[4] = { alpha, alpha, alpha, alpha } 或类似的。
  • 如果可能,考虑使用内在函数而不是内联汇编。这不太容易出错,并且允许编译器尽可能使用 SSE 指令来实现相同的代码。
  • 看起来您连续两次执行punpcklbw %%mm3, %%mm1,而不是进入 MM1 和 MM2。最初的 intel-syntax 源有 3 个解包,其中一个看起来很额外,就像它被重复以腾出空间来放更长的评论??!?
  • 您实际上可能最好用纯 C 编写此代码。GCC 可以很好地自动矢量化它。

标签: assembly x86-64 inline-assembly mmx


【解决方案1】:

您的问题是您试图将 alpha 值用作地址而不是值。 movd (%0), %%mm0 指令说使用%0 作为内存中的一个位置。所以你说加载alpha 指向的值而不是它的值。使用movd %0, %%mm0 可以解决这个问题,但是你会遇到alpha 值只有8 位类型并且它需要是32 位类型才能使用MOVD 指令的问题。您可以解决该问题,并且alpha 值需要乘以 256 并广播到目标寄存器的所有 4 个 16 位字,以便您的算法通过将其乘以 0x0100010001000100ULL 并使用 MOVQ 指令来工作。

但是,您根本不需要 MOVD/MOVQ 指令。您可以让编译器将值加载到 MMX 寄存器本身,方法是使用如下代码指定 y 约束:

typedef unsigned pixel;

static inline pixel
fade_pixel_mmx_asm(pixel p1, pixel p2, unsigned fade) {
    asm("punpcklbw %[zeros], %[p1]\n\t"
        "punpcklbw %[zeros], %[p2]\n\t"
        "psubw     %[p2], %[p1]\n\t"
        "pmulhw    %[fade], %[p1]\n\t"
        "paddw     %[p2], %[p1]\n\t"
        "packuswb  %[zeros], %[p1]"
        : [p1] "+&y" (p1), [p2] "+&y" (p2)
        : [fade] "y" (fade * 0x0100010001000100ULL), [zeros] "y" (0));
    return p1;
}

您会注意到这里不需要 clobber 列表,因为没有使用不是由编译器分配的寄存器,也没有编译器需要了解的其他副作用。我省略了必要的 EMMS 指令,因为您不想在每个像素上执行。您需要在混合两个表面的循环之后插入 asm("emms"); 语句。

更好的是,您根本不需要使用内联汇编。您可以改用内部函数,而不必担心使用内联汇编的所有缺陷:

#include <mmintrin.h>

static inline pixel
fade_pixel_mmx_intrin(pixel p1, pixel p2, unsigned fade) {
    __m64 zeros = (__m64) 0ULL;
    __m#64 mfade = (__m64) (fade * 0x0100010001000100ULL);
    __m64 mp1 = _m_punpcklbw((__m64) (unsigned long long) p1, zeros);
    __m64 mp2 = _m_punpcklbw((__m64) (unsigned long long) p2, zeros);

    __m64 ret;
    ret = _m_psubw(mp1, mp2);
    ret = _m_pmulhw(ret, mfade);
    ret = _m_paddw(ret, mp2);
    ret = _m_packuswb(ret, zeros);

    return (unsigned long long) ret;
}
    

与前面的示例类似,您需要在循环后调用_m_empty() 以生成必要的 EMMS 指令。

您还应该认真考虑只用纯 C 编写例程。如今,自动向量化器非常好,而且编译器使用现代 SIMD 指令生成的代码可能比您尝试使用古老的 MMX 指令生成的代码更好。例如这段代码:

static inline unsigned
fade_component(unsigned c1, unsigned c2, unsigned fade) {
    return c2  + (((int) c1 - (int) c2) * fade) / 256;
}

void
fade_blend(pixel *dest, pixel *src1, pixel *src2, unsigned char fade,
           unsigned len) {
    unsigned char *d = (unsigned char *) dest;
    unsigned char *s1 = (unsigned char *) src1;
    unsigned char *s2 = (unsigned char *) src2;
    unsigned i;
    for (i = 0; i < len * 4; i++) {
        d[i] = fade_component(s1[i], s2[i], fade);
    }
}

对于 GCC 10.2 和 -O3,上述代码生成的汇编代码使用 128 位 XMM 寄存器并在其内部循环中一次混合 4 个像素:

    movdqu  xmm5, XMMWORD PTR [rdx+rax]
    movdqu  xmm1, XMMWORD PTR [rsi+rax]
    movdqa  xmm6, xmm5
    movdqa  xmm0, xmm1
    punpckhbw       xmm1, xmm3
    punpcklbw       xmm6, xmm3
    punpcklbw       xmm0, xmm3
    psubw   xmm0, xmm6
    movdqa  xmm6, xmm5
    punpckhbw       xmm6, xmm3
    pmullw  xmm0, xmm2
    psubw   xmm1, xmm6
    pmullw  xmm1, xmm2
    psrlw   xmm0, 8
    pand    xmm0, xmm4
    psrlw   xmm1, 8
    pand    xmm1, xmm4
    packuswb        xmm0, xmm1
    paddb   xmm0, xmm5
    movups  XMMWORD PTR [rdi+rax], xmm0

最后,即使是未矢量化的 C 代码版本也可能接近最优,因为代码足够简单,无论混合的实现方式如何,您都可能会受到内存限制。

【讨论】:

  • (__m64) (fade * 0x0100010001000100ULL) 可能会比_mm_set1_pi16(fade) 更好。 (godbolt.org/z/faoos9) 当已知fade 已经正确地零扩展为 16 或 32 位时,它只是 movd + pshufw 而不是 movabs r64, imm64/imul。如果你把它写成乘法,clang 和 gcc 会错过这个优化——他们不会看数字来把它变成一个随机播放。 (clang 假设调用者将窄 args 扩展到 32 位,这与 GCC 不同)。
  • 我还玩弄了_mm_cvtsi32_si64 (movd) 以避免在movq mm0, rdi 之前的整数寄存器中的零扩展,但这使得clang movd 然后变为xmm movdq2q。不过,它似乎确实对 GCC 有所帮助,尽管它使用了 MMX 内在函数,但它对整个事情都使用了 XMM regs。
  • 但正如你所说,这主要是没有意义的:自动矢量化比这种手动矢量化版本更好,它只使用了一半的包吞吐量,而不用打扰punpckhbw
  • @PeterCordes 请注意,pshufw 来自 SSE。如果 SSE 不可用,则可以使用一对解包来代替。
  • @fuz:总的来说,这是一个 x86-64 问题,所以 SSE1 和 SSE2 保证可用。在 32 位 Pentium II(没有 SSE)上,一个 32x32 => 64 位乘法然后 2 个存储来馈送一个 movq 重新加载会导致存储转发停止,并且具有显着的延迟。 (或者 2x movd + punpckldq 的成本已经与 2 个解包相当)。在有序的 P5 Pentium-MMX 上,如果存储转发停顿是一件事,则为 IDK; in-order Atom 没有它们。但是mul r32 需要 9 个周期并且不可配对,所以这很糟糕,2 次解包肯定会更好。所以乘数不是很好。 ://
【解决方案2】:

在复制到 MM reg 之前,您可以使用标量乘以 0x0001000100010001ULLalpha 广播到 64 位。另一种选择是将movd 的8 位整数零扩展为32 位,然后pshufw 复制它。

您的 asm 也存在各种安全问题。

#include <SDL/SDL.h>
#include <stdint.h>

void fade_mmx(SDL_Surface* im1,SDL_Surface* im2,Uint8 alpha, SDL_Surface* imOut)
{
    int pixelsCount = imOut->w * im1->h;

    Uint32 *A = (Uint32*) im1->pixels;
    Uint32 *B = (Uint32*) im2->pixels;
    Uint32 *out = (Uint32*) imOut->pixels;
    Uint32 *end = out + pixelsCount;

    Uint64 alphas = (Uint64)alpha * 0x0001000100010001ULL;

    __asm__ __volatile__ (
            "\n\t movd  %0, %%mm0"
            "\n\t movd  %1, %%mm1"
            "\n\t movd  %2, %%mm2"
            "\n\t pxor       %%mm3, %%mm3"
            "\n\t punpcklbw  %%mm3, %%mm1"
            "\n\t punpcklbw  %%mm3, %%mm2"
            "\n\t psubw      %%mm2, %%mm1"
            "\n\t pmulhw     %%mm0, %%mm1"
            "\n\t paddw      %%mm2, %%mm1"
            "\n\t packuswb   %%mm3, %%mm1"
    : // you're probably going to want an "=m"(*something) memory output here
    : "r" (alphas), "m" (*A), "m" (*B), "r" (out), "r" (end)
    : "mm0", "mm1", "mm2", "mm3");
    __asm__("emms" : : );
}

如果编译器知道所有输入和输出,则 asm 语句不需要是 volatile,而不是依赖于 "memory" clobber。 (就像这里一样,没有输出,只读取作为输入操作数的寄存器和内存。)

对于 32 位代码,将 "r"(alphas) 替换为 "m"(alphas)。或者使用"rm"(alphas) 让编译器选择。 (但是对于 32 位,使用 pshufw 肯定更好,而不是让编译器将 64 位乘法结果存储为 2 个 32 位的一半,然后在使用 movq 重新加载它时遭受存储转发停顿。内在会留下决定使用_mm_set1_epi16(alpha) 发送给编译器,尽管无论如何你只能在循环外执行一次)。

请注意,我还添加了必要的 clobber 列表并将包含您取消引用的指针的寄存器操作数替换为引用您取消引用的内存的内存操作数,从而允许 gcc 推断您访问的内存

请注意,如果您不解决这些问题,gcc 将会不高兴并且您的代码行为将是未定义的,可能会以神秘且难以调试的方式失败。除非您完全了解自己在做什么,否则不要使用内联汇编。考虑使用内在函数作为更安全且可能更有效的替代方案。 (https://gcc.gnu.org/wiki/DontUseInlineAsm)。

带有__m128i 向量的 SSE2 可以轻松地一次处理 4 个像素,而不是 2 或 1 个像素通过填充零而浪费您的 pack 吞吐量的一半。 (使用punpckhbw 补充punpcklbw 来设置)。 MMX 已经过时,以至于现代 CPU 对某些指令的 MMX 版本的吞吐量低于等效的 128 位 SSE2 XMM 指令的吞吐量。

【讨论】:

  • 这仍然缺少一些重要的东西来保证它的安全:告诉编译器这读取指向的内存 (How can I indicate that the memory *pointed* to by an inline ASM argument may be used?),它会破坏 mmx regs (从而破坏 x87 状态) - 可能只是 "mm0", "mm1" etc would work. Also missing doing anything with the result in mm1; an "=x"(vec)` 输出上的破坏者可以工作。
  • 但是说真的,gcc.gnu.org/wiki/DontUseInlineAsm - 这不是开始翻译的最佳方式。使用_mm_unpacklo_pi16,或者更好的是一次使用 SSE2 来处理 1、2 或 4 个像素,因为 x86-64 可以保证这一点。 (当然使用 unpackhi 和 lo,并进行大量负载)。嗯,实际的 asm 看起来有问题,连续执行两次 punpcklbw %%mm3, %%mm1;这是 OP 需要解决的另一个问题。
  • @PeterCordes 我知道。鉴于 OPs sn-p 非常不完整,我认为这只是为了制作一个 MCVE,稍后会添加正确的 clobber。
  • 我认为让 unsafe inline asm 在 Stack Overflow 上不加注释是个坏主意。如果我们假设看到它的每个人都知道这是一个无声问题的雷区,那么肯定会有未来的读者将其视为示例并按原样使用它,也许添加一个输出操作数但不 i> 注册或"memory"clobbers。
  • @PeterCordes 请注意,gcc 和 clang 已经开始在支持的情况下使用 SSE 指令实现 MMX 内在函数,因此如果 OP 只是移动到内在函数,那么无论哪种情况都会生成好的代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-01
  • 2020-11-21
  • 2021-03-25
  • 1970-01-01
相关资源
最近更新 更多