我正在为我的 3D 计算开发优化,现在我有:
plain
使用标准 C 语言库的 " " 版本,SSE
使用预处理器编译的优化版本#define USE_SSE
,AVX
使用预处理器编译的优化版本#define USE_AVX
是否可以在 3 个版本之间切换而无需编译不同的可执行文件(例如,拥有不同的库文件并动态加载“正确”的文件,不知道inline
函数是否“正确”)?我还会考虑在软件中使用这种开关的性能。
我正在为我的 3D 计算开发优化,现在我有:
plain
使用标准 C 语言库的 " " 版本,SSE
使用预处理器编译的优化版本#define USE_SSE
,AVX
使用预处理器编译的优化版本#define USE_AVX
是否可以在 3 个版本之间切换而无需编译不同的可执行文件(例如,拥有不同的库文件并动态加载“正确”的文件,不知道inline
函数是否“正确”)?我还会考虑在软件中使用这种开关的性能。
有几种解决方案。
一种是基于 C++,您可以在其中创建多个类 - 通常,您实现一个接口类,并使用工厂函数为您提供正确类的对象。
例如
class Matrix
{
virtual void Multiply(Matrix &result, Matrix& a, Matrix &b) = 0;
...
};
class MatrixPlain : public Matrix
{
void Multiply(Matrix &result, Matrix& a, Matrix &b);
};
void MatrixPlain::Multiply(...)
{
... implementation goes here...
}
class MatrixSSE: public Matrix
{
void Multiply(Matrix &result, Matrix& a, Matrix &b);
}
void MatrixSSE::Multiply(...)
{
... implementation goes here...
}
... same thing for AVX...
Matrix* factory()
{
switch(type_of_math)
{
case PlainMath:
return new MatrixPlain;
case SSEMath:
return new MatrixSSE;
case AVXMath:
return new MatrixAVX;
default:
cerr << "Error, unknown type of math..." << endl;
return NULL;
}
}
或者,如上所述,您可以使用具有通用接口的共享库,并动态加载正确的库。
当然,如果您将 Matrix 基类实现为“普通”类,您可以逐步改进并仅实现您真正认为有益的部分,并依靠基类来实现性能不是非常关键的功能。
编辑:您谈论的是内联,如果是这种情况,我认为您正在查看错误的功能级别。你需要相当大的函数来处理相当多的数据。否则,您将花费所有精力将数据准备为正确的格式,然后执行一些计算指令,然后将数据放回内存中。
我还会考虑您如何存储数据。您是在存储包含 X、Y、Z、W 的数组集合,还是在单独的数组中存储大量 X、大量 Y、大量 Z 和大量 W [假设我们正在进行 3D 计算]?根据您的计算方式,您可能会发现采用一种或另一种方式会给您带来最大的好处。
我已经做了相当多的 SSE 和 3DNow!几年前的优化,“技巧”通常更多地是关于如何存储数据,以便您可以轻松地一次性获取正确类型数据的“捆绑”。如果您以错误的方式存储数据,您将浪费大量时间“混合数据”(将数据从一种存储方式移动到另一种存储方式)。
一种方法是实现三个符合相同接口的库。使用动态库,您只需交换库文件,可执行文件将使用它找到的任何内容。例如在 Windows 上,您可以编译三个 DLL:
然后针对Impl.dll
. 现在只需将三个特定 DLL 之一放入与 相同的目录中.exe
,将其重命名为Impl.dll
,它将使用该版本。同样的原则应该基本上适用于类 UNIX 操作系统。
下一步是以编程方式加载库,这可能是最灵活的,但它是特定于操作系统的,需要更多工作(如打开库、获取函数指针等)
编辑:但是,当然,您可以只实现该功能三次并在运行时选择一个,具体取决于某些参数/配置文件设置等,如其他答案中所述。
当然有可能。
最好的方法是让函数完成完整的工作,并在运行时在它们中进行选择。这可行但不是最佳的:
typedef enum
{
calc_type_invalid = 0,
calc_type_plain,
calc_type_sse,
calc_type_avx,
calc_type_max // not a valid value
} calc_type;
void do_my_calculation(float const *input, float *output, size_t len, calc_type ct)
{
float f;
size_t i;
for (i = 0; i < len; ++i)
{
switch (ct)
{
case calc_type_plain:
// plain calculation here
break;
case calc_type_sse:
// SSE calculation here
break;
case calc_type_avx:
// AVX calculation here
break;
default:
fprintf(stderr, "internal error, unexpected calc_type %d", ct);
exit(1);
break
}
}
}
每次通过循环时,代码都在执行一个switch
语句,这只是开销。一个非常聪明的编译器理论上可以为您修复它,但最好自己修复它。
相反,编写三个单独的函数,一个用于普通函数,一个用于 SSE,一个用于 AVX。然后在运行时决定运行哪一个。
对于奖励积分,在“调试”构建中,使用 SSE 和平原进行计算,并断言结果足够接近以提供信心。写明文版本,不是为了速度,而是为了正确;然后使用它的结果来验证你聪明的优化版本是否得到了正确的答案。
传奇人物约翰卡马克推荐后一种方法;他称之为“并行实现”。阅读他的文章。
所以我建议你先写普通版。然后,返回并开始使用 SSE 或 AVX 加速重写部分应用程序,并确保加速版本给出正确的答案。(有时,普通版本可能存在加速版本没有的错误。拥有两个版本并比较它们有助于发现任何一个版本的错误。)