繁体   English   中英

使用C / C ++在相同的可执行文件中进行不同的优化(plain,SSE,AVX)

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

我正在为我的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,大量的W和许多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和plain进行计算,并声明结果足够接近以给出置信度。 写简单版本,不是为了速度,而是为了正确; 然后使用其结果来验证您的智能优化版本是否得到正确的答案。

传奇的约翰卡马克推荐后一种方法; 他称之为“并行实施”。 阅读关于它的文章

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM