需要一个优雅的SSE2方法来预乘Alpha然后将Alpha设置为1.0f

时间:2016-04-29 15:50:21

标签: visual-c++ sse

我正在使用Visual Studio 2015,构建x64代码,并使用四个ABGR像素值的浮点矢量,即最高位置的Alpha(不透明度)和较低位置的蓝色,绿色和红色数字三个职位。

我正在尝试编写一个PreMultiplyAlpha例程,该例程将内联/ __ vectorcall执行有效的工作,将alpha预乘为蓝色,绿色和红色,并在完成时将Alpha值设置为1.0f。

实际乘法没问题。这会将Alpha传播到所有四个元素,然后将它们全部相乘。

__m128 Alpha = _mm_shuffle_ps(Pixel, Pixel, _MM_SHUFFLE(3, 3, 3, 3));
__m128 ReturnPixel = _mm_mul_ps(Pixel, Alpha);

使用上述内容,alpha将乘以所有颜色,并使用最少的指令:

shufps  xmm1, xmm0, 255             ; 000000ffH
mulps   xmm1, xmm0

这是一个很好的开始,对吧?

然后我碰到了一堵砖墙...我没有发现一种直接的方式 - 甚至是一种棘手的方式 - 做一些似乎应该是一个相当简单的行为,有效地将最重要的元素(Alpha)设置为1.0 F。也许我只是有一个盲点。

最明显的方法是使VC ++ 2015创建执行两次128位内存访问的机器代码:

ReturnPixel.m128_f32[ALPHA] = 1.0f;

上面生成这样的代码,它将整个像素保存在堆栈中,覆盖Alpha,然后从堆栈中加载它:

movaps  XMMWORD PTR ReturnPixel$1[rsp], xmm1
mov     DWORD PTR ReturnPixel$1[rsp+12], 1065353216 ; 3f800000H
movaps  xmm1, XMMWORD PTR ReturnPixel$1[rsp]

我非常喜欢让代码尽可能直接让人类维护人员理解,但是这个特殊的例程被大量使用,需要以最快的速度制作。

我尝试过的其他事情似乎导致编译器发出更多指令(特别是内存访问),而不是必要的......

这会尝试将A位置移动到最不重要的单词中,将其替换为1.0f,然后将其移回。它非常好,但确实可以从内存位置获取一个32位的1.0f。

ReturnPixel = _mm_shuffle_ps(ReturnPixel, ReturnPixel, _MM_SHUFFLE(0, 2, 1, 3));
ReturnPixel = _mm_move_ss(ReturnPixel, _mm_set_ss(1.0f));
ReturnPixel = _mm_shuffle_ps(ReturnPixel, ReturnPixel, _MM_SHUFFLE(0, 2, 1, 3));

我得到了这些指示:

movss   xmm0, DWORD PTR __real@3f800000
movaps  xmm1, xmm0
shufps  xmm2, xmm2, 39              ; 00000027H
movss   xmm2, xmm1
shufps  xmm2, xmm2, 39

有没有想法如何在A字段(最重要的元素)中留下1.0f,只需要最少的指令,理想情况下除了从指令流中取出的内容之外没有额外的内存访问?我甚至考虑将矢量单独划分为在所有位置都达到1.0f,但我对分歧过敏,因为它们至少可以说效率低......

提前感谢您的想法。 : - )

-Noel

2 个答案:

答案 0 :(得分:2)

1.0 float常量必须来自某个地方,因此必须加载或generated on the fly。没有SSE等效于fld1,编译器通常会使用更少的指令,即使存在D-cache未命中而不是mov eax, 0x3f800000 / movd xmm0, eax或其他东西的风险。 (参见Agner Fog's Optimizing Assembly,第13.4节有关序列表。生成1.0需要3个insn。)

没有SSE / SSE2单指令可以替换向量的32b元素(低元素的其他movss)。 SSE4.1引入了insertpspinsrd。使用两个pinsrw指令一次设置16b不太可能是最佳选择,尤其是。如果你想将该矢量输入FP计算。

如果你想存储它,那么最好两个重叠的存储最好:用错误的数据存储16B向量,然后存储1.0。理论上,智能编译器会将其编译为shufps-broadcast / mulps / movaps [mem], xmm1 / mov [mem+12], 0x3f800000。但是,如果您从[mem]开始执行向量加载,则会导致存储转发停顿。 (对于典型搜索的存储/重载往返,另外~10个周期的延迟高于正常~5c)

处理常量

由于您正在处理像素,我认为这意味着这种情况发生在具有多次迭代的循环中。这意味着我们正在优化循环中的效率,即使这意味着在循环外部进行一些额外的设置。

一个好的编译器会在内联后将循环从循环中提升出来,因此将操作分解为一个函数,使用_mm_set_ps_mm_set1_ps作为常量。你应该检查asm; MSVC doesn't always manage to do this,因此您可能需要手动内联和提升。

在寄存器中,为进一步的FP操作做准备

如果我们想要在regs中使用向量,那么重叠存储选项是不可行的。 (我们应该这样做:我们仍然能够以足够低的成本做到这一点,因为它不能证明对数据进行单独的循环以应用alphas。)

替换高元素的最便宜选项是blendps_mm_blend_ps)。具有即时控制操作数的混合在SSE4.1和后来支持它们的CPU上非常有效:1c延迟,并且可以在SnB上的多个执行端口上运行,因此它们不会在特定执行端口上产生瓶颈。 (可变混合物更贵)。 insertps(_ mm_insert_ps`)功能更强大(例如,可以在dest中选择零元素,并从src中的任何元素中选择),但需要shuffle端口。

如果没有SSE4.1,我们最好的选择可能是两条指令:用AND屏蔽高元素,然后用1.0 [ 1.0 0 0 0 ]向量屏蔽1.0f。 0.0f的IEEE表示为全零,因此我们可以安全地进行OR而不影响低元素。这只是2条说明。

andpsorps都只能在Intel Nehalem上的port5(与shufps竞争)上运行到Broadwell。 Skylake在p015上运行它们,与pandpor相同。如果吞吐量成为瓶颈而不是延迟,请考虑使用整数指令(转换为__m128i)。使用por的输出作为addps的输入时,只需额外1个周期的旁路延迟(Intel SnB系列)。

__m128 apply_alpha(__m128 Pixel) {
    __m128 Alpha = _mm_shuffle_ps(Pixel, Pixel, _MM_SHUFFLE(3, 3, 3, 3));
    __m128 Multiplied = _mm_mul_ps(Pixel, Alpha);
#ifdef __SSE4_1__
    // blendps imm8 is cheaper (runs on more ports) than insertps on Intel SnB-family
    __m128 Alpha_Reset = _mm_blend_ps(Multiplied, _mm_set1_ps(1.0), 1<<3);
#else
    // emulate the blend with AND/OR
    const __m128 zeroalpha_mask = _mm_castsi128_ps( _mm_set_epi32(0,~0,~0,~0) );  // could be generated with pcmpeqw / psrldq 4
    __m128 Alpha_Reset = _mm_and_ps(Multiplied, zeroalpha_mask);
    const __m128 alpha_one = _mm_set_ps(1.0, 0, 0, 0);
    Alpha_Reset = _mm_or_ps(Alpha_Reset, alpha_one);
#endif
    return Alpha_Reset;
}

在循环中调用它可以很好地使用gcc:它在循环外的寄存器中设置它的所有常量,所以在循环中只是一个加载,一些寄存器操作和一个存储。

Godbolt Compiler Explorer上查看我的测试循环的来源。您还可以使用-march=haswell来启用它支持的所有指令集,包括-msse4.1,并查看blendps版本也可以编译。

loop(float __vector(4)*):
    movaps  xmm4, XMMWORD PTR .LC0[rip] # setup of constants hoisted out of the loop
    lea     rax, [rdi+160000]
    movaps  xmm3, XMMWORD PTR .LC1[rip]
    movaps  xmm2, XMMWORD PTR .LC3[rip]
.L3:
    movaps  xmm1, XMMWORD PTR [rdi]
    add     rdi, 16
    # apply_alpha inlined beginning here
    movaps  xmm0, xmm1                 # This is the insn you forgot to include in the question, for your shufps broadcast without AVX.  It's unavoidable, but still counts
    shufps  xmm0, xmm1, 255
    mulps   xmm0, xmm1
    andps   xmm0, xmm4
    orps    xmm0, xmm3
    # and ends here
    addps   xmm0, xmm2                 # extra add outside of apply_alpha, otherwise a scalar store to set alpha may be better
    movaps  XMMWORD PTR [rdi-16], xmm0
    cmp     rax, rdi
    jne     .L3
    ret

将此扩展到256b向量也很容易:仍然使用具有两倍宽度的常量的混合,一次做2个像素。

答案 1 :(得分:1)

感谢所有回复的人,我们选择了一个只进行一次128位内存访问的解决方案,而不是我最初列出的三个直接代码:

//  Ensures the result of the multiply leaves a 0 in Alpha.
__m128 ABGZ = _mm_move_ss(Pixel, _mm_setzero_ps());
__m128 ZAAA = _mm_shuffle_ps(ABGZ, ABGZ, _MM_SHUFFLE(0, 3, 3, 3));
__m128 ReturnPixel = _mm_mul_ps(Pixel, ZAAA);
ReturnPixel = _mm_or_ps(ReturnPixel, _mm_set_ps(1.0f, 0, 0, 0));

这将生成以下代码:

xorps   xmm1, xmm1
movss   xmm2, xmm1
shufps  xmm2, xmm2, 63              ; 0000003fH
mulps   xmm2, xmm0
orps    xmm2, XMMWORD PTR __xmm@3f800000000000000000000000000000

我曾希望有一个可能以编程方式生成1.0f的解决方案并保持此代码的所有注册工作。那好吧。毫无疑问,这个128位值将被缓存。

未来的某一天,当我们将产品提升到SSE4.1的最低支持水平时,我们会重新审视这一点。

-Noel