我可以用 Vector<T> 做校验算术吗
Can I do checked arithmetic with Vector<T>
我一直在尝试 Vector 使用硬件来并行化整数运算。有什么方法可以启用矢量操作的溢出检查吗?
一个例子是将两列(等长数组)的整数加在一起。这里c=a+b
表示c[0] = a[0] + b[0]
、c[1] = a[1] + b[1]
等
我想我可以这样做:
overflow[i] = b[i] >= 0 ? c[i] < a[i] : c[i] >= a[i];
但是这个(分支)可能比 .Net 的自动溢出检查慢,并且可能抵消使用 Vector<T>
.
的性能优势
我们还想优化我们最常用的运算:乘法、减法,以及较小程度的整数除法。
编辑: 我仔细考虑了一下,想出了这个,它比未经检查的向量加法慢 2.5 倍。似乎有很多额外的开销。
public Vector<int> Calc(Vector<int> a, Vector<int> b)
{
var result = a + b;
var overflowFlag = Vector.GreaterThan(b, Vector<int>.Zero) * Vector.LessThan(result,a)
+ Vector.LessThan(b,Vector<int>.Zero) * Vector.GreaterThan(result, a);
// It makes no sense to add the flags to the result, but haven't decided what to do with them yet,
// and don't want the compiler to optimise the overflow calculation away
return result + overflowFlag;
}
计时:(4k 次迭代添加一对 100k 数组)
- 正常添加:618ms
- 正常检查添加:1092ms
- 向量加法:208ms
- 向量检查添加:536ms
使用从 Hacker's Delight(第 2 章,溢出检测部分)借来的一些技巧,这里有一些溢出谓词(未测试):
有符号加法:
var sum = a + b;
var ovf = (sum ^ a) & (sum ^ b);
结果是标志,而不是完整的面具。也许这就足够了,也许还不够,在这种情况下,我通常会建议右移,但 Vector<T>
上没有右移(缺少太多东西)。不过,您可以与零进行比较。
无符号加法:为了完整性?
var sum = a + b;
var ovf = Vector.LessThan(sum, a);
乘法:
据我所知,没有合理的方法可以做到这一点。即使在原生 SSE 中也有点烦人,但是 pmuldq
和一些改组它还不错。
在 C# SIMD 中,这似乎毫无希望。没有 high-mul(除了 16 位整数外,本机 SSE 也没有,这也很烦人),没有扩大乘法(无论如何也没有办法缩小结果),也没有合理的提前扩大的方法。即使你可以扩大(他们能否请认真地将其添加到 API),,如此烦人以至于用标量算术来做它不是一个坏的选择,这违背了这一点。
所以我建议不要在 SIMD 中这样做,至少在 C# 中不要这样做。
这并不一定意味着您使用内置的溢出检测。虽然如果溢出是一个致命错误,这是合适的,但如果它是常见的和预期的并且您只想在布尔标志中显示溢出状态,那么它会非常慢。在这种情况下,您可以使用:
有符号乘法:
long ext_prod = (long)a * b;
int prod = (int)ext_prod;
bool ovf = (prod >> 31) != (int)(ext_prod >> 32);
无符号乘法:
ulong ext_prod = (ulong)a * b;
uint prod = (uint)ext_prod;
bool ovf = (ext_prod >> 32) != 0;
在 SIMD 中,它的工作方式基本相同,即测试高半部分是否填充了符号的副本(根据定义,在无符号情况下为零),但加宽使其在原生 SIMD 中变得烦人且无望在 C# SIMD 中。
我一直在尝试 Vector 使用硬件来并行化整数运算。有什么方法可以启用矢量操作的溢出检查吗?
一个例子是将两列(等长数组)的整数加在一起。这里c=a+b
表示c[0] = a[0] + b[0]
、c[1] = a[1] + b[1]
等
我想我可以这样做:
overflow[i] = b[i] >= 0 ? c[i] < a[i] : c[i] >= a[i];
但是这个(分支)可能比 .Net 的自动溢出检查慢,并且可能抵消使用 Vector<T>
.
我们还想优化我们最常用的运算:乘法、减法,以及较小程度的整数除法。
编辑: 我仔细考虑了一下,想出了这个,它比未经检查的向量加法慢 2.5 倍。似乎有很多额外的开销。
public Vector<int> Calc(Vector<int> a, Vector<int> b)
{
var result = a + b;
var overflowFlag = Vector.GreaterThan(b, Vector<int>.Zero) * Vector.LessThan(result,a)
+ Vector.LessThan(b,Vector<int>.Zero) * Vector.GreaterThan(result, a);
// It makes no sense to add the flags to the result, but haven't decided what to do with them yet,
// and don't want the compiler to optimise the overflow calculation away
return result + overflowFlag;
}
计时:(4k 次迭代添加一对 100k 数组)
- 正常添加:618ms
- 正常检查添加:1092ms
- 向量加法:208ms
- 向量检查添加:536ms
使用从 Hacker's Delight(第 2 章,溢出检测部分)借来的一些技巧,这里有一些溢出谓词(未测试):
有符号加法:
var sum = a + b;
var ovf = (sum ^ a) & (sum ^ b);
结果是标志,而不是完整的面具。也许这就足够了,也许还不够,在这种情况下,我通常会建议右移,但 Vector<T>
上没有右移(缺少太多东西)。不过,您可以与零进行比较。
无符号加法:为了完整性?
var sum = a + b;
var ovf = Vector.LessThan(sum, a);
乘法:
据我所知,没有合理的方法可以做到这一点。即使在原生 SSE 中也有点烦人,但是 pmuldq
和一些改组它还不错。
在 C# SIMD 中,这似乎毫无希望。没有 high-mul(除了 16 位整数外,本机 SSE 也没有,这也很烦人),没有扩大乘法(无论如何也没有办法缩小结果),也没有合理的提前扩大的方法。即使你可以扩大(他们能否请认真地将其添加到 API),
所以我建议不要在 SIMD 中这样做,至少在 C# 中不要这样做。
这并不一定意味着您使用内置的溢出检测。虽然如果溢出是一个致命错误,这是合适的,但如果它是常见的和预期的并且您只想在布尔标志中显示溢出状态,那么它会非常慢。在这种情况下,您可以使用:
有符号乘法:
long ext_prod = (long)a * b;
int prod = (int)ext_prod;
bool ovf = (prod >> 31) != (int)(ext_prod >> 32);
无符号乘法:
ulong ext_prod = (ulong)a * b;
uint prod = (uint)ext_prod;
bool ovf = (ext_prod >> 32) != 0;
在 SIMD 中,它的工作方式基本相同,即测试高半部分是否填充了符号的副本(根据定义,在无符号情况下为零),但加宽使其在原生 SIMD 中变得烦人且无望在 C# SIMD 中。