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

问题描述 投票:5回答:2

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

#[repr(C, align(16))]
pub struct Vector(pub [f32; 3]);

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

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);
}

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

rust floating-point sse simd floating-point-exceptions
2个回答
5
投票

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

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

使用AVX512,您可以使用零掩码抑制元素的异常,但是直到那时为止,还没有这样的功能。此外,AVX512还允许您覆盖舍入模式+不遮盖所有异常(SAE),因此您可以使最接近偶数显式获得SAE。但这会抑制all元素的异常。


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

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

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


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

即使清除然后再用fenv填充物检查粘滞的FP掩蔽标志,也很少,并且需要在受控的情况下使用。我对库函数调用没有任何期望,尤其是没有使用任何SIMD的期望。


在垃圾元素中避免subnormals

如果MXCSR没有设置FTZ和DAZ,则您可以从次常态(也称为非常态)中放慢速度。 (即正常情况,除非您使用-ffast-math的Rust等效符号进行编译。)对于具有SSE / AVX指令的典型x86硬件,

产生NaN或+ -Inf不需要花费额外的时间

。 (有趣的事实:NaN也很慢,即使在现代硬件上也具有x87数学功能)。因此,例如,在进行数学运算之前,以_mm_or_ps结果为cmpps是安全的,可以在向量的某些元素中创建NAN。或使用_mm_and_ps在除数之前在除数中创建一些零。

但是要注意填充中的垃圾是什么,因为它可能导致虚假的次正常状态。

0.0和NaN(全都是)通常都是安全的。

通常避免在SIMD中使用水平填充。 SIMD vec!=几何vec。

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

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

如果您只是将这个部分矢量填充物用于奇数操作的剩余元素,那就太好了

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

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


免费获取零

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

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

[使用AVX,如果您担心填充会导致不正常的情况,则可以考虑使用屏蔽负载。 https://www.felixcloutier.com/x86/vmaskmov。但这比vmovaps慢一些。而且蒙版stores

在AMD上甚至更贵,甚至在Ryzen上也是如此。

[像在C语言中一样,在Rust中,sizeof始终是alignof的倍数:这是必须的,因为sizeof用作数组中的stride

,并且数组元素需要正确对齐。

因此,即使struct仅使用12个字节,但其sizeof仍然是16个字节,带有4个字节的“填充”。

因此,我提出了一个非常实用的解决方案:属于填充。与其让struct的内部结构可见,还不如为其提供构造函数和访问器...,并使用1.0值将其填充为16个字节。

#[repr(C, align(16))]
pub struct Vector([f32; 4]);

impl Vector {
    pub fn new(v: [f32; 3]) -> Vector {
        Vector([v[0], v[1], v[2], 1.0])
    }

    pub fn pad(&mut self, pad: f32) { self.0[3] = pad; }

    pub fn as_ptr(&self) -> *const f32 { self.0.as_ptr() }
}

然后,您可以放心地执行操作,不使用垃圾字节。


4
投票

[像在C语言中一样,在Rust中,sizeof始终是alignof的倍数:这是必须的,因为sizeof用作数组中的stride

,并且数组元素需要正确对齐。
© www.soinside.com 2019 - 2024. All rights reserved.