在游戏引擎数学库中使用函数指针实现SIMD——一个好主意吗?

3
我从14岁开始阅读游戏引擎书籍(当时我什么也不懂:P)。现在已经过了好多年,我想开始为我的游戏引擎编写数学基础。我已经长时间考虑如何设计这个“库”(我指的是“有组织的文件集”)。每隔几年就会出现新的SIMD指令集,我不想让它们浪费掉。(如果我对此有误,请告诉我。)
我希望至少具备以下特性:
- 能够在运行时检查是否具有SIMD,并在具有SIMD时使用SIMD,在没有时使用正常的C++版本。(可能会有一些调用开销,这值得吗?) - 如果我们已知目标在编译时能够编译为SIMD或正常的C++。调用可以被内联并适用于交叉优化,因为编译器知道是否使用了SIMD或C++。
编辑-我想使源代码可移植,以便在除x86(-64)之外的其他设备上运行。
因此,我认为使用函数指针会是一个好的解决方案,我会将其设置为静态并在程序开始时进行初始化。适当的函数(例如矩阵/向量的乘法)将调用它们。
你认为这种设计的优缺点是什么(哪个更重要?)?是否可能创建具有上述两个属性的函数指针?

你可能想要查看编译器内置函数。有许多函数直接映射到SIMD指令,如果目标平台不支持特定的指令,今天的编译器应该足够聪明以模拟该指令。在这里查看VS2012内部函数的例子。 - Some programmer dude
SSE2指令已经至少可用了10年。你确定你关心在旧设备上运行的游戏吗(假设我们不是在谈论手机等移动设备上的游戏)? - Mats Petersson
@MatsPetersson 我编辑了我的问题!我忘记了非常重要的目标,即它必须可移植到几乎任何系统。 - user896326
@JoachimPileborg 我觉得模拟很好,但如果我使用VS2012的编译器内置函数,我的代码就会与特定编译器相关。 - user896326
如果您使用SIMD扩展,那么您将拥有一些“不可移植”的代码。内置函数实际上是相当可移植的,因为它们可以与VS、GCC和Intel编译器一起使用。显然,在ARM或PowerPC处理器上不行... - Mats Petersson
@MatsPetersson 我明白。我希望它能够在运行时和编译时(用于内联)决定它们是否受支持,这样就可以在ARM或PowerPC上使用它。 - user896326
2个回答

5

在决定调用哪个程序时,选择正确的粒度非常重要。如果您在太低的级别上这样做,函数分派开销就会成为一个问题,例如,一个仅具有几条指令的小程序可能会因通过某种函数指针分派机制而变得非常低效,而不是直接内联。理想情况下,架构特定的程序应该处理合理数量的数据,使函数分派成本可以忽略不计,而又不会过大导致编译每种支持的架构的额外非架构特定代码时出现显著的代码膨胀。


由于数学库中的大多数函数都相当小,您是否建议放弃其在运行时能够推断是否具有SIMD属性,并在可用时使用它?并将使用SIMD的决策移动到更高级别?我确信这样做会更有效率,但这可能不可避免地会导致代码膨胀。 - user896326
通常为了从SIMD中获得任何真正的好处,每个例程应该在一个相当大的数据集上运行,即不仅是执行 SIMD 向量的加法等操作,而是执行 1D 或 2D 数组的加法。这样你就可以通过例程本身获得良好的效率,而函数调度开销也不会很大。看一下英特尔在其 IPP 库中如何处理这个问题。 - Paul R
我能在英特尔(Intel)的网站上找到IPP库,但是您是否有该库的内部链接呢?您会建议我如何设计我的数学库? - user896326
1
如果您阅读IPP的在线文档,它会谈到调度机制。至于设计,那是一个大课题,但基本上就是我上面说的:设计库函数,使其在足够大的数据集上运行,例如,在图像处理中,一行、瓦片或块的像素。理想情况下,您应该处理适合缓存的数据块,以便在数据仍在缓存中时组合多个函数调用。 - Paul R

0

最简单的方法是编译两次游戏,一次启用 SIMD,一次不启用。创建一个小型启动器应用程序,执行 _may_i_use_cpu_feature 检查,然后运行正确的编译版本。

通过函数指针调用矩阵乘法(例如)所引起的双重间接性不会很好。它不会内联微不足道的数学函数,而是会在所有场所引入函数调用,并且这些调用将被强制保存/恢复许多寄存器以引导(因为指针后面的代码在运行时无法得知)。

此时,没有双重间接的未经优化版本将大大优于具有函数指针的 SSE 版本。

至于支持多个平台,这可能很容易,也可能是真正的麻烦。 ARM neon 与 SSE4 相似,值得在某些宏后面包装指令,但是 neon 也有足够的不同之处,以至于非常恼人!

#if CPU_IS_INTEL

#include <immintrin.h>
typedef __m128 f128;

#define add4f _mm_add_ps

#else

#include <neon.h>
typedef float32x4 f128;

#define add4f vqadd_f32

#endif

从Intel开始,然后稍后移植到ARM的主要问题是很多好东西不存在。在ARM上可以进行洗牌,但这也很麻烦。除法、点积和平方根在ARM上不存在(只有倒数估计,您需要自己进行牛顿迭代)

如果您正在考虑像这样的SIMD:

struct Vec4 
{
  float x;
  float y;
  float z;
  float w;
};

如果你想要在编程中使用SSE和NEON,那么你可能只需要用一个半好的包装器来封装它们。但是当涉及到AVX512和AVX2时,你可能会遇到困难。

如果你打算使用结构数组格式来进行SIMD操作:

struct Vec4SOA
{
  float x[BIG_NUM];
  float y[BIG_NUM];
  float z[BIG_NUM];
  float w[BIG_NUM];
};

如果你幸运的话,可能会能够制作出 AVX2/AVX512 版本。然而,使用这样组织的代码并不是世界上最容易的事情。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接