- 一个使用标准C语言库的“普通”版本, - 一个使用预处理器
#define USE_SSE
编译的SSE
优化版本,
- 一个使用预处理器#define USE_AVX
编译的AVX
优化版本。是否可以在不编译不同可执行文件的情况下切换这3个版本(例如,拥有不同的库文件并动态加载“正确”的库文件,不知道是否应该使用
inline
函数来实现)?我也会考虑在软件中进行此类切换时的性能。#define USE_SSE
编译的SSE
优化版本,
- 一个使用预处理器#define USE_AVX
编译的AVX
优化版本。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;
}
}
或者,如上所建议的,您可以使用具有共同接口的共享库,并动态加载正确的库。
当然,如果您将矩阵基类实现为您的“普通”类,则可以进行逐步细化并仅实现您实际发现有益的部分,并依赖于基类来实现性能不高的函数。
编辑: 您谈论内联,我认为如果是这种情况,您正在查看错误的函数级别。您需要相当大的函数,在相当多的数据上执行某些操作。否则,您所有的努力都将花费在将数据准备成正确格式上,然后执行少量计算指令,然后将数据放回到内存中。
我还会考虑如何存储数据。您是否存储了具有X、Y、Z、W数组集的集合,还是将大量X、大量Y、大量Z和大量W分别存储在单独的数组中[假设我们正在进行三维计算]?根据您的计算方式,您可能会发现采用一种或另一种方式将为您带来最大的好处。
我曾经做过一些SSE和3DNow!优化,几年前,“诀窍”通常更多地与如何存储数据有关,以便您可以轻松地一次性获取正确类型的数据“捆绑”。如果您将数据存储在错误的方式中,那么您将浪费大量时间“扫描数据”(将数据从一种存储方式移动到另一种)。
-march=i7
编译,即使是 C 版本也只能在 i7 上运行;如果使用 -march=i686
进行编译,则可以在过去 15 年内构建的每台机器上运行,但某些内部函数(如 SSE/AVX)将不可用,并且优化器仅使用 SSE/AVX 版本中可用指令的子集。 - Gunther PiezImpl.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和普通计算结果,并断言结果足够接近以获得信心。编写普通版本,不是为了速度,而是为了正确性;然后使用其结果验证您的优化版本是否得到正确答案。
传奇人物John Carmack推荐后一种方法;他称之为“并行实现”。阅读他的文章了解更多信息。
所以我建议您先编写普通版本。然后,回过头来使用SSE或AVX加速重新编写应用程序的部分,并确保加速版本给出正确的答案。(有时,普通版本可能存在加速版本没有的错误。拥有两个版本并进行比较可以帮助发现任何一个版本中的错误。)
switch
分支调用的函数内部。 - lethal-guitar