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

Posted

技术标签:

【中文标题】使用 C/C++ 在同一个可执行文件中进行不同的优化(plain、SSE、AVX)【英文标题】:Have different optimizations (plain, SSE, AVX) in the same executable with C/C++ 【发布时间】:2013-01-18 21:43:43 【问题描述】:

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

使用标准 C 语言库的“plain”版本, 使用预处理器#define USE_SSE 编译的SSE 优化版本, 使用预处理器#define USE_AVX 编译的AVX 优化版本

是否可以在 3 个版本之间切换而无需编译不同的可执行文件(例如,拥有不同的库文件并动态加载“正确”的文件,不知道 inline 函数是否“正确”) ? 我也会考虑在软件中使用这种开关的性能。

【问题讨论】:

没有提到平台?即使您知道永远不会调用这些指令,某些平台也会拒绝使用 avx 运行代码。一些平台有 ifunc 在运行时在多个实现之间进行选择。一些平台在依赖于功能的路径中寻找共享库。 【参考方案1】:

对此有多种解决方案。

一个基于 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!几年前的优化,“技巧”通常更多是关于如何存储数据,以便您可以轻松地一次性获取正确类型数据的“捆绑”。如果您以错误的方式存储数据,您将浪费大量时间“混合数据”(将数据从一种存储方式移动到另一种存储方式)。

【讨论】:

这种方法的问题是不能针对不同的架构编译优化不同的函数。如果全部使用 -march=i7 编译,即使 C 版本也只能在 i7 上运行,如果使用 -march=i686 编译,它将在过去 15 年构建的每台机器上运行,但某些内在函数(如 SSE/AVX)将不可用,优化器将仅使用 SSE/AVX 版本中可用指令的子集。 所以在单独的源文件中构建代码。虽然我发现如果你真的想以非常好的方式使用 SSE/AVX 指令,你将需要使用内联汇编程序。编译器通常在“聪明”方面做得不好。【参考方案2】:

一种方法是实现三个符合相同接口的库。使用动态库,您只需交换库文件,可执行文件将使用它找到的任何内容。例如在 Windows 上,您可以编译三个 DLL:

PlainImpl.dll SSEImpl.dll AVXImpl.dll

然后针对Impl.dll 建立可执行链接。现在只需将三个特定 DLL 之一放入与 .exe 相同的目录中,将其重命名为 Impl.dll,它将使用该版本。同样的原则应该基本上适用于类 UNIX 操作系统。

下一步是以编程方式加载库,这可能是最灵活的,但它是特定于操作系统的,需要更多工作(如打开库、获取函数指针等)

编辑: 但是,当然,您可以只实现该功能三次并在运行时选择一个,具体取决于某些参数/配置文件设置等,如其他答案中所述。

【讨论】:

在 Linux 上,加载库和获取函数指针的样板工作可以通过 Implib.so tool 自动化。【参考方案3】:

当然可以。

最好的方法是让函数完成完整的工作,并在运行时在它们中进行选择。这可行,但不是最佳的:

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 和普通版进行计算,并断言结果足够接近以提供信心。写明文版本,不是为了速度,而是为了正确;然后用它的结果来验证你聪明的优化版本是否得到了正确的答案。

传奇人物约翰卡马克推荐后一种方法;他称之为“并行实现”。阅读his essay 了解它。

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

【讨论】:

如果您正在考虑优化,我怀疑您是否希望在循环中进行此类检查... 是的,您宁愿将循环放在为每个switch 分支调用的函数中。 或者更好的是,有一个接口类,它使用 3 种优化......多态开关进行扩展和实现。

以上是关于使用 C/C++ 在同一个可执行文件中进行不同的优化(plain、SSE、AVX)的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C 中找到可执行文件的位置? [复制]

一个编译生成的可执行程序是用啥命令执行

带有 GCC 的 C/C++:将资源文件静态添加到可执行文件/库

外部程序(可执行文件)调用的一些问题

从 z/OS 上的 C 语言可执行文件中删除符号信息

Linux调用可执行程序