整数矢量化精度/整数除法精度是否取决于 CPU?

Is integer vectorization accuracy / precision of integer division CPU-dependent?

提问人:György Kőszeg 提问时间:3/14/2023 最后编辑:György Kőszeg 更新时间:3/14/2023 访问量:147

问:

我尝试对 16 位整数 ARGB 通道的 64 位颜色进行矢量化。

我很快意识到,由于缺乏加速整数除法支持,我需要将我的值转换为并显式使用一些 SSE2/SSE4.1 内部函数以获得最佳性能。尽管如此,我还是想将非特定的通用版本保留为后备解决方案(我知道它目前比某些普通操作慢,但它将为可能的改进提供未来的兼容性)。float

但是,结果在我的机器上是不正确的。

一个非常小的重现:

// Test color with 50% alpha
(ushort A, ushort R, ushort G, ushort B) c = (0x8000, 0xFFFF, 0xFFFF, 0xFFFF);

// Minimal version of the fallback logic if HW intrinsics cannot be used:
Vector128<uint> v = Vector128.Create(c.R, c.G, c.B, 0u);
v = v * c.A / Vector128.Create(0xFFFFu);
var cPre = (c.A, (ushort)v[0], (ushort)v[1], (ushort)v[2]);

// Original color:
Console.WriteLine(c); // prints (32768, 65535, 65535, 65535)

// Expected premultiplied color:   (32768, 32768, 32768, 32768)
Console.WriteLine(cPre); // prints (32768, 32769, 32769, 32769)

我试图确定发出了哪些指令导致不准确,但我真的很惊讶地发现,在 SharpLab 中,结果是正确的。另一方面,该问题在 .NET Fiddle 中是可重现的。

这是某些平台上预期的内容,还是我应该在运行时存储库中将其报告为错误?


更新

没关系,这显然是一个错误。使用其他值会导致完全错误的结果:

using System;
using System.Numerics;
using System.Runtime.Intrinsics;

(ushort A, ushort R, ushort G, ushort B) c = (32768, 65535, 32768, 16384);

Vector128<uint> v1 = Vector128.Create(c.R, c.G, c.B, 0u);
v1 = v1 * c.A / Vector128.Create(0xFFFFu);

// prints <32769, 49152, 57344, 0> instead of <32768, 16384, 8192, 0>
Console.WriteLine(v1);

// Also for the older Vector<T>
Span<uint> span = stackalloc uint[Vector<uint>.Count];
span[0] = c.R;
span[1] = c.G;
span[2] = c.B;
Vector<uint> v2 = new Vector<uint>(span) * c.A / new Vector<uint>(0xFFFF);

// prints <32769, 49152, 57344, 0, 0, 0, 0, 0> on my machine
Console.WriteLine(v2);

最后我意识到问题出在乘法上:如果我代入常数表达式,那么结果是正确的。由于某种原因,该值未从打包字段中正确提取/屏蔽(?)。甚至受到影响:* c.A* 32768ushortVector.Create

(ushort A, ushort R, ushort G, ushort B) c = (32768, 65535, 32768, 16384);

Console.WriteLine(Vector128.Create((int)c.A)); // -32768
Console.WriteLine(Vector128.Create((int)32768)); // 32768
Console.WriteLine(Vector128.Create((int)c.A, (int)c.A, (int)c.A, (int)c.A)); // 32768

更新 2

最后在运行时存储库中提交了一个问题

C# 精度 SIMD 自动矢量化

评论

1赞 György Kőszeg 3/14/2023
与此同时,我意识到 SharpLab 在调试模式下也会产生错误的结果。奇怪,因为在我的计算机上,调试和发布版本都不正确。所以我开始相信这毕竟是一个错误。
1赞 Peter Cordes 3/14/2023
这可以通过整数乘法和移位来完成,尽管它不是非常有效,因为它需要 32x32 => 64 位乘法的高一半,而 SSE2 / AVX 只给你一个给你完整的 64 位结果。所以每个向量只有一半的输入元素。godbolt.org/z/7E9P9aWMh 显示了 GCC 和 clang,使用乘法逆将 4x 的向量除以 。这是精确的整数除法,在任何时候都不涉及 FP。pmuludquint32_t0xffffu
1赞 canton7 3/14/2023
我在软件回退路径中没有看到任何会导致这种情况的内容......我很想在 dotnet/runtime repo 中提出这个问题,或者在 discord 上询问(很多 JIT 人在那里闲逛,他们会告诉你是否需要打开一个问题)#allow-unsafe-blocks
2赞 György Kőszeg 3/14/2023
@PeterCordes:是的,使用普通代码的位移比除法更快,但结果并不完全相同。浮点版本现在运行良好,这只是我注意到的问题。
2赞 Peter Cordes 3/14/2023
@GyörgyKőszeg:我链接的编译器输出是针对 .结果是精确的,就像我说的那样使用乘法逆,就像编译器对标量所做的那样。为什么 GCC 在实现整数除法时使用奇数乘法?你以为我是说吗?哦,你可能以为我的意思是用整数乘法来做你表达式的一部分;我说的是使用整数乘法的高半部分作为除法的一部分。vec / 0xffffuuint / constantv >> 16*

答: 暂无答案