需要一个优雅的 SSE2 方法来预乘 Alpha,然后将 Alpha 设置为 1.0f
Posted
技术标签:
【中文标题】需要一个优雅的 SSE2 方法来预乘 Alpha,然后将 Alpha 设置为 1.0f【英文标题】:Need an Elegant SSE2 Method for Premultiplying Alpha then Setting Alpha to 1.0f 【发布时间】:2016-04-29 15:50:21 【问题描述】:我正在使用 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,但我对除法过敏,因为它们至少可以说效率低下...
提前感谢您的想法。 :-)
-诺埃尔
【问题讨论】:
如果您使用位掩码执行&
会怎样,例如11111110000000
然后 |
代表浮点数 1?
假设 SSE4.1 然后看看 _mm_insert_ps
? (糟糕——刚刚看到 SSE2 的要求——这有点苛刻......)
@PaulR: _mm_blend_ps
实际上是最有效的方式。 insertps
只能在 shuffle 端口上运行。但是对于 SSE2,@flatmouse 的建议似乎是最好的。两个 insn 而不是一个,只要你的编译器可以将常量的设置提升到循环之外就很好了。
【参考方案1】:
1.0 float
常量必须来自某个地方,因此必须加载或 generated on the fly。没有与 fld1
等效的 SSE,编译器通常会使用更少的指令,即使存在 D-cache 未命中的风险,而不是 mov eax, 0x3f800000
/ movd xmm0, eax
或其他东西。 (有关序列表,请参阅Agner Fog's Optimizing Assembly,第 13.4 节。生成 1.0 需要 3 个 insns)。
没有 SSE/SSE2 单指令可以替换向量的 32b 元素(其他 movss
用于低元素)。 SSE4.1 引入了insertps
和pinsrd
。使用两个 pinsrw
指令一次设置 16b 不太可能是最佳选择,尤其是。如果你想将该向量输入到 FP 计算中。
如果要存储它,那么最好使用两个重叠存储:存储 16B 向量和错误数据,然后存储 1.0.0。理论上,智能编译器会将其编译为 shufps-broadcast / mulps / movaps [mem], xmm1
/ mov [mem+12], 0x3f800000
。但是,如果您立即从[mem]
执行向量加载,则会导致存储转发停止。 (对于典型 uarch 的存储/重新加载往返,在正常的 ~5c 之上还有 ~10 个延迟周期)
处理常量
由于您正在处理像素,我认为这意味着这发生在具有多次迭代的循环中。这意味着我们正在优化循环中的效率,即使这意味着在循环之外进行一些额外的设置。
一个好的编译器会在内联后将常量从循环中提升出来,因此将操作分解为使用_mm_set_ps
或_mm_set1_ps
作为其常量的函数应该没问题。不过,您应该检查 asm; MSVC doesn't always manage to do this,所以你可能需要手动内联和提升。
在寄存器中,为进一步的 FP 操作做准备
如果我们想在 regs 中有向量时继续使用向量,则重叠存储选项不可行。 (我们应该这样做:我们仍然可以以足够低的成本做到这一点,以至于它不能证明对数据进行单独循环以应用 alpha 是合理的)。
替换高元素最便宜的选择是blendps
(_mm_blend_ps
)。与直接控制操作数的混合在支持它们的 SSE4.1 和更高版本的 CPU 上非常有效:1c 延迟,并且可以在 SnB 及更高版本的多个执行端口上运行,因此它们不会在特定执行端口上产生瓶颈。 (可变混合物更昂贵)。 insertps
(_mm_insert_ps`) 更强大(例如,可以将 dest 中的选定元素归零,并从 src 中的任何元素中选取),但需要 shuffle 端口。
如果没有 SSE4.1,我们最好的选择可能是两条指令:用 AND 屏蔽高元素,然后在 [ 1.0 0 0 0 ]
的向量中的 1.0f 中进行 OR。 0.0f
的 IEEE 表示是全零,因此我们可以安全地进行 OR 而不影响低元素。这只是两条指令。
andps
和 orps
都只在 Intel Nehalem 到 Broadwell 的 port5(与 shufps 竞争)上运行。 Skylake 在 p015 上运行它们,与 pand
和 por
相同。如果吞吐量被证明是瓶颈,而不是延迟,请考虑改用整数指令(转换为__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 个向量也很容易:仍然使用具有两倍宽的常量的 blendps 来一次处理 2 个像素。
【讨论】:
【参考方案2】:感谢所有回复的人,我们确定了一种解决方案,它只执行一次 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 的最低支持级别时,我们会重新审视这一点。
-诺埃尔
【讨论】:
这很好,你避免了andps
并且它与一个常量中的movss xmm,xmm
是常量,编译器足够聪明,可以即时生成。请记住,regs 之间的movss
只能在 shuffle 端口上运行(Haswell 上的 port5 开始)。如果您的代码在 shuffle 吞吐量方面遇到瓶颈,请考虑使用 andps
。如果您的编译器可以将常量加载从循环中提升出来,那很好。以上是关于需要一个优雅的 SSE2 方法来预乘 Alpha,然后将 Alpha 设置为 1.0f的主要内容,如果未能解决你的问题,请参考以下文章
ILC_COLOR32 图像列表中的图像是不是经过 alpha 预乘?我收到相互矛盾的信息
iOS之深入解析预乘透明度Premultiplied Alpha
是否可以制作一个 24 bpp 且没有 alpha 通道的 NSBitmapImageRep?
关于 Alpha is Transparency 到底需不需要勾的最终结论