在同一个可执行文件中使用C/ c++进行不同的优化(plain、SSE、AVX)

Have different optimizations (plain, SSE, AVX) in the same executable with C/C++

本文关键字:优化 plain AVX SSE 可执行文件 同一个 c++      更新时间:2023-10-16

我正在为我的3D计算开发优化,现在我有:

  • 使用标准C语言库的"plain"版本,
  • 一个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:

  • PlainImpl.dll
  • SSEImpl.dll
  • AVXImpl.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语句,这只是开销。一个真正聪明的编译器理论上可以为你修复它,但最好自己修复它。

相反,应该编写三个独立的函数,一个用于plain,一个用于SSE,一个用于AVX。然后在运行时决定运行哪一个。

对于额外的积分,在"调试"构建中,同时使用SSE和平面进行计算,并断言结果足够接近以给出信心。写简单的版本,不是为了速度,而是为了正确性;然后使用它的结果来验证你的聪明的优化版本得到正确的答案。

传奇人物John Carmack推荐后一种方法;他称之为"并行实现"。读他关于这方面的文章。

所以我建议你先写普通版本。然后,回头开始使用SSE或AVX加速重写应用程序的某些部分,并确保加速版本给出正确的答案。(有时候,普通版本可能有加速版本没有的bug。有两个版本并比较它们有助于发现任何一个版本中的bug。