在奇数大小的对齐向量上使用“安全” SIMD算法?

L11*_*117 7 floating-point sse simd floating-point-exceptions rust

假设我有一些16字节对齐的结构,只包装3xFloat32数组:

#[repr(C, align(16))]
pub struct Vector(pub [f32; 3]);
Run Code Online (Sandbox Code Playgroud)

现在,我想将其划分为两个实例,如下所示:

use core::arch::x86_64;

let a = Vector([1f32, 2f32, 3f32]);
let b = Vector([4f32, 5f32, 6f32]);
let mut q = Vector([0f32, 0f32, 0ff32]);

unsafe {
    let a1 = x86_64::_mm_load_ps(a.0.as_ptr());
    let b1 = x86_64::_mm_load_ps(b.0.as_ptr());
    let q1 = x86_64::_mm_div_ps(a1, b1);
    x86_64::_mm_store_ps(q.0.as_mut_ptr(), q1);
}
Run Code Online (Sandbox Code Playgroud)

它可以进行除法,但是有一个问题:第4个元素包含垃圾,除其他外,垃圾可以发出NaN信号。并且,如果未屏蔽某些例外标志,则将触发SIGFPE。我想以某种方式避免这种情况,而不会完全沉默信号。即我要么只想在第4对元素上使其静音,要么在其中添加一些合理的值。最好,最快的方法是什么?也许总体上有更好的方法?

Pet*_*des 5

通常,没有人会掩盖FP异常,否则,您需要改组以例如复制元素之一,以便顶部元素与其他元素之一进行相同的划分。或有其他已知的安全之物。

如果您可以假设股息不是该元素的NaN,那么也许只需要对除数进行改组就可以摆脱困境。

使用AVX512,您可以使用零遮罩抑制元素的异常,但在那之前还没有这种功能。此外,AVX512还允许您覆盖舍入模式+抑制所有异常(SAE),而不进行屏蔽,因此您可以使最接近偶数的值显式获得SAE。但这抑制了所有元素的异常。


认真地说,请勿启用FP例外。如果异常的数量是明显的副作用,编译器几乎/不知道如何以安全的方式进行优化。例如-ftrapping-math,默认情况下,GCC 处于打开状态,但已损坏。

我不认为LLVM会更好。默认的严格FP可能仍会进行优化,使一个SIGFPE的源将提高2或4。甚至可能优化将其提高0的源将提高1,反之亦然,例如GCC破损且几乎无用的默认设置。

但是,如果您希望永远都没有某种异常,则启用FP异常对于调试可能会很有用。但是您可以通过忽略具有该源地址的SIMD指令来处理偶尔出现的误报。


如果要在性能和异常正确性之间进行权衡,则大多数库用户宁愿将性能最大化。

甚至清除并检查满载粘性FP遮罩标记的fenv情况很少,并且需要在受控的情况下使用。我对库函数调用没有任何期望,尤其是没有使用任何SIMD的函数。


避免在垃圾元素中使用非正规元素

如果MXCSR没有设置FTZ和DAZ,则您可以从次常态(也称为非常态)中放慢速度。(即正常情况,除非您使用(等效于Rust)进行编译-ffast-math。)

对于带有SSE / AVX指令的典型x86硬件,生成NaN或+ -Inf不需要花费额外的时间。(有趣的事实:NaN也很慢,即使在现代硬件上也具有x87数学功能)。所以,它的安全_mm_or_pscmpps效果的数学运算之前向量的某些元素,打造一个NAN,例如。或者_mm_and_ps在除法之前在除数中创建一些零。

但是要小心填充中的垃圾,因为它可能导致虚假的超常态。 0.0和NaN(全都是)通常都是安全的。


通常避免使用SIMD横放东西。SIMD vec!=几何vec。

通常,仅使用SIMD向量的4个元素中的3个是个坏主意,因为这通常意味着您使用的是单个SIMD向量来保存单个几何向量,而不是使用3个向量的4个x坐标,4个y坐标和4个z坐标。

随机播放/水平填充通常需要额外的指令(除了已在内存中存储的标量的广播负载外),但是如果您以这种方式使用SIMD,则经常需要大量随机播放。在某些情况下,您无法对一系列事物进行矢量化处理,但仍可以通过SIMD加快速度。

如果您只是将此部分向量填充物用于奇数大小操作的剩余元素,那么great,一个部分向量将比3个标量迭代好得多。但是大多数询问仅使用4个向量元素中的3个的人都在问,因为他们错误地使用了SIMD,例如,添加几何向量作为SIMD向量仍然很便宜,但是点积需要改组。请参阅https://deplinenoise.wordpress.com/2015/03/06/slides-simd-at-insomniac-games-gdc-2015/了解有关如何正确使用SIMD的一些不错的知识(SoA与AoS等上)。如果您已经知道这一点,并且只是将3元素向量用于奇数角点情况,而不是大部分工作,那么就可以了。

通常,对于奇数大小,填充为矢量宽度的倍数通常是很好的选择,但是对于某些算法,另一个选择是最终的未对齐矢量,该矢量在数据末尾结束。除非是就地算法,否则部分重叠的存储会很好,并且您必须担心不会重复执行一次元素。(或者关于甚至用于等价运算(例如AND遮罩或钳位)的存储转发停顿)。


免费获得零

如果只剩下2个float元素,则movsd加载将加载+零扩展到XMM寄存器中。您也可以让编译器代替movaps

否则,如果将3个标量改组在一起,则insertps可以将元素置零。或者,您可能movss从内存中的负载中得知xmm reg的零高部分。因此,对于编译器,将a 0.0作为标量向量初始化器(如C ++ _mm_set_ps())的一部分可以免费使用。

使用AVX时,如果您担心填充会导致不正常的变化,则可以考虑使用屏蔽负载。 https://www.felixcloutier.com/x86/vmaskmov。但这有点慢vmovaps。蒙面商店在AMD甚至是Ryzen上都更加昂贵。