rust - 奇数大小对齐向量的“安全”SIMD算法?
问题描述
假设我有一些 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 对元素上使其静音,要么放一些理智的值。最好、最快的方法是什么?或者也许有更好的方法?
解决方案
通常没有人揭露 FP 异常,否则您需要洗牌以复制其中一个元素,以便顶部元素与其他元素之一进行相同的划分。或者有其他一些已知的安全的东西。
如果您可以假设除数在该元素中是非 NaN,那么您可能只需要改组除数即可。
使用 AVX512,您可以使用零掩码抑制元素的异常,但在此之前没有这样的功能。此外,AVX512 允许您覆盖舍入模式 + 抑制所有异常 (SAE) 而无需屏蔽,因此您可以使最接近甚至显式地获得 SAE。但这会抑制所有元素的异常。
说真的,不要启用 FP 异常。如果异常数量是可见的副作用,编译器几乎/不知道如何以安全的方式进行优化。例如 GCC-ftrapping-math
默认是开启的,但是它已经坏掉了。
我不会认为 LLVM 会更好。默认的严格 FP 可能仍然会进行优化,可以在源会引发 2 或 4 的情况下给出一个 SIGFPE。甚至可能会在源会引发 1 的情况下引发 0 的优化,反之亦然,例如 GCC 的损坏且几乎无用的默认值。
但是,如果您希望永远不会出现任何某种异常,则启用 FP 异常可能对调试很有用。但是您可以通过忽略具有该源地址的指令来处理 SIMD 指令中偶尔出现的误报。
如果在性能和异常正确性之间进行权衡,那么库的大多数用户宁愿它最大化性能。
即使用东西清除然后检查粘性 FP 掩码标志fenv
也很少这样做,并且需要在受控情况下才能使用。我对库函数调用没有任何期望,尤其是对使用任何 SIMD 的函数调用没有任何期望。
避免垃圾元素中的次正规
如果 MXCSR 没有设置 FTZ 和 DAZ,您可能会因次正规(也称为非正规)而减速。(即正常情况,除非您使用 (Rust 等价的) 编译-ffast-math
。)
对于具有 SSE / AVX 指令的典型 x86 硬件,生成 NaN 或 +-Inf 不需要额外的时间。(有趣的事实:NaN 也很慢,即使在现代硬件上也有 x87 数学的遗留问题)。因此,例如,在数学运算之前在向量的某些元素中创建 NAN是安全_mm_or_ps
的。cmpps
或者_mm_and_ps
在除法之前在除数中创建一些零。
但是要小心填充中的垃圾,因为它可能会导致虚假的次常态。 0.0
和 NaN(全为)通常总是安全的。
通常避免使用 SIMD 进行横向处理。SIMD vec != 几何向量。
仅使用 SIMD 向量的 4 个元素中的 3 个通常是一个坏主意,因为这通常意味着您使用单个 SIMD 向量来保存单个几何向量,而不是 4 个x
坐标、4 个y
坐标和 4 个z
坐标的三个向量。
洗牌/水平的东西大多需要额外的指令(除了已经在内存中的标量的广播负载),但如果你以这种方式使用 SIMD,你通常需要大量的洗牌。在某些情况下,您无法对一系列事物进行矢量化,但您仍然可以通过 SIMD 获得加速。
如果您只是将这个部分向量的东西用于奇数大小的操作的剩余元素,那么很好,一个部分向量比 3 次标量迭代要好得多。但是大多数人询问只使用 4 个向量元素中的 3 个是因为他们使用 SIMD 错误,例如添加几何向量作为 SIMD 向量仍然很便宜,但点积需要洗牌。有关如何正确使用SIMD的一些好东西(SoA 与 AoS 等等上)。如果您已经知道这一点并且只是将 3 元素向量用于奇怪的极端情况,而不是大多数工作,那很好。
填充到向量宽度的倍数对于奇数大小通常很好,但某些算法的另一种选择是在数据末尾结束的最终未对齐向量。部分重叠的存储很好,除非它是一个就地算法并且你不得不担心没有两次做一个元素。(或者关于存储转发停止,即使对于像 AND 屏蔽或钳位这样的幂等操作)。
免费获得零
如果您只剩下 2 个float
元素,movsd
则加载将加载 + 零扩展到 XMM 寄存器中。您不妨让编译器来代替movaps
.
否则,如果将 3 个标量混在一起,insertps
可以将元素归零。或者您可能movss
从内存加载中知道 xmm regs 的零高部分。因此,编译器可以免费使用 a0.0
作为标量向量初始化程序的一部分(如 C++ )。_mm_set_ps()
使用 AVX,如果您担心填充会导致不正常,可以考虑使用屏蔽负载。 https://www.felixcloutier.com/x86/vmaskmov。但这比vmovaps
. 蒙面商店在 AMD 上要贵得多,甚至是 Ryzen。
推荐阅读
- javascript - 通过分页从 Rest API 调用加载所有数据需要太长时间,并且在加载所有内容之前无法执行任何操作
- python - 什么是解析具有不同数据类型的 JSON 响应的好方法?
- node.js - TypeError:无法读取未定义的 Youtube 数据 API 身份验证 NodeJS 的属性“redirect_uris”
- react-native - 反应原生顶部标签栏导航器:指示器宽度以匹配文本
- python - “黑名单”或从 Python 函数的返回中删除某些内容?
- undefined - 打开 index.html 但结果是未定义
- javascript - browser.runtime.sendMessage:“接收端不存在”
- python - 随机森林分类如何在幕后工作?
- javascript - 在轮播控件中单击按钮打开表单
- mongodb - 如何在事务期间从 MongoDB 创建操作中检索 id?