【问题标题】:How to check overflow for multiplication of 16 bit integers in SSE?如何检查 SSE 中 16 位整数乘法的溢出?
【发布时间】:2019-03-06 23:47:14
【问题描述】:

我想在 SSE 中实现一个简单的功能(像Izhikevich spiking neuron model 这样的程序)。它应该适用于 16 位有符号整数(8.8 定点),并且需要在某个积分步骤中检查溢出条件,并设置 SSE 掩码(如果发生溢出):

// initialized like following:
short I = 0x1BAD; // current injected to neuron
short vR = 0xF00D; // some reset threshold when spiked (negative)

// step to be vectorized:
short v0 = vReset;
for(;;) {

    // v0*v0/16 likely overflows => use 32 bit (16.16)
    short v0_sqr = ((int)v0)*((int)v0) / (1<<(8+4)); // not sure how "(v0*v0)>>(8+4)" would affect sign..
     // or   ((int)v0)*((int)v0) >> (8+4); // arithmetic right shift
     // original paper used v' = (v0^2)/25 + ...

    short v1 = v0_sqr + v0 + I;
    int m; // mask is set when neuron fires
    if(v1_overflows_during_this_operation()) { // "v1 > 0x7FFF" - way to detect?
        m=0xFFFFFFFF;
    else
        m=0;
    v0 = ( v1 & ~m ) | (vR & m );
}

但我还没有找到_mm_mul_epi16() 指令来检查乘法的高位字。为什么以及如何在 SSE 中实现 v1_overflows_during_this_operation() 这样的任务?

【问题讨论】:

  • 你确定不是short v1 = (v0*v0&gt;&gt;6) + v0 + I;?假设这是真正的 C,+ 的优先级高于&gt;&gt;,所以你写了v1 = v0*v0 &gt;&gt; (6 + v0 + I)。 NVM,你在我评论的时候就编辑了。
  • @PeterCordes 抱歉,我已经解决了问题。在原始论文中,它就像v' = (v0^2)/25 + ...。不过后来我可以将定点乘法和除法混合成 32 位运算。
  • SSE 没有 16x16=>32 位乘法。它具有单独的低/高半乘法指令。希望我们可以为溢出检查做一次乘法运算,为实际结果做一次乘法运算。
  • 您只担心上次编辑的标志,对吧? (因为 C 不保证负整数的算术右移)。您实际上并不需要有符号除法舍入语义(向 0 截断)而不是算术右移(向 -Inf 舍入),对吗?
  • 见鬼!获得另一位或两位精度可能会使您的性能降低 2 倍,因为您必须计算另一个乘法结果来检查溢出,或者有效地扩大到 32 位(将每个向量的元素数量减半)。

标签: c vectorization sse simd intrinsics


【解决方案1】:

与 32x32 => 64 不同,没有扩展 16x16 -> 32 SSE 乘法指令。

相反,_mm_mulhi_epi16 and _mm_mulhi_epu16 只为您提供完整结果的有符号或无符号上半部分。

(和_mm_mullo_epi16,它会打包 16x16 => 16 位低半截断乘法,这对于有符号或无符号都是相同的)。

您可以使用_mm_unpacklo/hi_epi16 将低/高半部分交织成一对具有 32 位元素的向量,但这会非常慢。但是,是的,您可以将 _mm_srai_epi32(v, 8+4) 算术右移 12,然后重新打包,可能使用 _mm_packs_epi32(有符号饱和回到 16 位)。那我猜要检查饱和度?


您的用例不寻常。 _mm_mulhrs_epi16 为您提供高 17 位,四舍五入然后截断为 16 位。 (见说明)。这对于一些定点算法很有用,其中输入被缩放以将结果放在上半部分,并且您希望舍入包括下半部分而不是截断。

您实际上可能使用_mm_mulhrs_epi16_mm_mulhi_epi16 作为保持最精确的最佳选择,也许通过左移您的v0,然后平方到高半部分将为您提供(v0*v0) &gt;&gt; (8+4) 的点。

那么你认为不让结果溢出,只像作者在原论文中那样用_mm_cmpge_epi16(v1, vThreshold) 生成掩码更容易吗?

是的!获得另一位或两位精度可能会使您的性能降低 2 倍,因为您必须计算另一个乘法结果来检查溢出,或者有效地扩大到 32 位(将每个向量的元素数量减半),如上所述。

通过比较结果,v0 = ( v1 &amp; ~m ) | (vR &amp; m ); 变为 SSE4.1 混合:_mm_blendv_epi8


如果您的 vThreshold 在顶部有 2 个未设置的位,则您有空间左移而不会丢失任何最重要的位。由于mulhi 给了你(v0*v0) &gt;&gt; 16,所以你可以这样做:

// losing the high 2 bits of v0
__m128i v0_lshift2   = _mm_slli_epi16(v0, 2);    // left by 2 before squaring
__m128i v0_sqr_asr12 = _mm_mulhi_epi16(v0_lshift2, v0_lshift2);
__m128i v1 = _mm_add_epi16(v0, I);
        v1 = _mm_add_epi16(v1, v0_sqr_asr12);

    // v1 = ((v0<<2)* (int)(v0<<2))) >> 16) + v0 + I

    // v1 = ((v0*(int)v0) >> 12) + v0 + I

平方前左移 2 与平方后左移 4 相同(完整的 32 位结果)。它将我们想要的 16 位准确地放入高 16 位。

但是,如果您的 v0 非常接近全范围,以至于您在左移时可能会溢出,则这是不可用的。

否则,在乘法之前可能会丢失v0的6个低位

使用算术右移向 -Infinity 舍入会损失 6 位精度,但不可能溢出。

// losing the low 6 bits of v0
__m128i v0_asr6 = _mm_srai_epi16(v0, 6);
__m128i v0_sqr_asr12 = _mm_mullo_epi16(v0_asr6, v0_asr6);
__m128i v1 = _mm_add_epi16(v0, I);
        v1 = _mm_add_epi16(v1, v0_sqr_asr12);

    // v1 =  (v0>>6) * (int)(v0>>6)) + v0 + I

    // v1 ~= ((v0*(int)v0) >> 12) + v0 + I

我认为这样会损失更多的精度,因此最好将vThreshold 设置得足够小,以便有足够的开销来使用高半乘法。这种方式包括可能更糟糕的舍入。

pmulhrsw 舍入而不是截断可能会更好,如果我们可以高效地设置它。但我认为我们不能,因为右移 1 是奇数。我认为我们需要进行 2 个单独的输入,一个 v0_lshift2 和一个仅左移 1。

【讨论】:

  • 是的,你是对的。低位无关紧要,因为所有“神经材料”主要是关于积累统计数据。只有这个二次项的大小才重要,确定 - “细胞现在是激发还是抑制” 对于给定电流 Iv0
  • 感谢混合!你能给我推荐一本好书吗,我可以在哪里阅读所有关于 SSE 常见做法的信息?
  • @xakepp35:我推荐 Agner Fog 的 x86 asm 优化手册 (agner.org/optimize)。他关于 SIMD 的章节有一张非常漂亮的表格,其中包含用于各种目的的有用数据移动指令(shuffles/blends)。此外,如果您忽略 AVX512,则没有那么多 的 SSE 指令。当我希望找到一个有用的指令时,我曾经只是查看 x86 asm 指令列表。 felixcloutier.com/x86/index.html。 (asm 指令集手册在底部列出了内在函数,或者使用我链接的内在函数查找器。它也按类别细分)。
  • @xakepp35:你试过我的内部代码短块了吗?我很确定它可以满足您的要求,左移以提供高半乘法而不是右移以提供低半乘法。除了四舍五入发生时,这些是完全等价的。我的回答确实已经提到了_mm_mullo_epi16,但我建议不要使用它。只要您的班次计数正确,二进制定点就可以工作;如果您的某些中间结果的点不在同一位置,也没关系。
  • 我认为您使用v0&lt;&lt;2 的第一个解决方案更好,只需在确定“如果这会在下一次迭代中触发(溢出)?”之后检查掩码m=cmpgt_epi16(v1, 0x1FFF) 即可。 - 并使用混合将其重置为vReset
猜你喜欢
  • 2012-01-01
  • 2012-05-17
  • 2013-07-25
  • 1970-01-01
  • 2015-03-30
  • 2012-01-21
  • 2015-07-29
  • 2020-06-12
  • 2023-03-09
相关资源
最近更新 更多