如何有条件地为模板头设置编译器优化

How to conditionally set compiler optimization for template headers

我找到了 ,并继续尝试回答它。作者想用 AVX 优化编译一个源文件(依赖于模板库),而项目的其余部分没有这些。

所以,为了看看会发生什么,我创建了一个这样的测试项目:

main.cpp

#include <iostream>
#include <string>
#include "fn_normal.h"
#include "fn_avx.h"

int main(int argc, char* argv[])
{   
    int number = 10; // this will come from input, but let's keep it simple for now
    int result;

    if (std::string(argv[argc - 1]) == "--noavx")
        result = FnNormal(number);
    else
    {
        std::cout << "AVX selected\n";
        result = FnAVX(number);
    }

    std::cout << "Double of " << number << " is " << result << std::endl;

    return 0;
}

文件fn_normal.hfn_avx.h包含函数FnNormal()和[=15的声明=]分别定义如下:

fn_normal.cpp

#include "fn_normal.h"
#include "double.h"

int FnNormal(int num)
{
    return RtDouble(num);
}

fn_avx.cpp

#include "fn_avx.h"
#include "double.h"

int FnAVX(int num)
{
    return RtDouble(num);
}

这里是模板函数定义:

double.h

template<typename T>
int RtDouble(T number)
{
    // Side effect: generates avx instructions
    const int N = 1000;
    float a[N], b[N];
    for (int n = 0; n < N; ++n)
    {
        a[n] = b[n] * b[n] * b[n];
    }    
    return number * 2;
}


最终,我将 "Properties-> C/C++ -> Code Generation" 下的文件 fn_avx.cpp 设置为 Enhanced Instruction SetAVX,将其设置为 Not Set其他来源,因此它应该默认为 SSE2。

我认为通过这样做,编译器会为包含它的每个源实例化一次模板(并避免通过破坏模板函数名称或其他方式违反单一定义规则),从而调用带有 --noavx 参数的程序可以在没有 avx 支持的情况下 运行 在 cpus 中正常工作。
但是生成的程序实际上只有一个机器代码版本的函数,带有 avx 指令,并且会在旧的 cpus 上失败。

禁用所有其他优化并不能解决此问题。还尝试 No Enhanced Instructions - /arch:IA32 而不是 Not Set

由于我刚刚开始了解模板等,有人可以向我指出此行为的确切细节以及我实际上可以做些什么来实现我的目标吗?

我的编译器是 MSVC 2013。


附加信息: fn_normal.cppfn_avx.cpp 的 .obj 文件字节大小几乎相同。我查看了生成的程序集列表,发现它们几乎相同,重要的区别是启用 avx 的源分别用 vmovssvmulss 替换了默认 sse 的 movss/mulss。但是在 Visual Studio 的反汇编视图中单步执行代码 (Ctrl+Alt+D), 确认 fnNormal() 确实使用了 avx 专用指令。

基本上,编译器需要尽量减少 space 没有提到如果有静态成员,将相同的模板实例化 2x 可能会导致问题。因此,据我所知,编译器要么为每个源代码处理模板,然后选择其中一个实现,要么将实际代码生成推迟到 link 时间。无论哪种方式,这都是这个 AVX 东西的问题。我最终以老式的方式解决了它——使用一些不依赖于任何模板或任何东西的全局定义。对于过于复杂的应用程序,这可能是一个大问题。 Intel Compiler 有一个最近添加的编译指示(我不记得确切的名字),它使得函数在它仅使用 AVX 指令后立即实现,这将解决问题。到底有多靠谱,我也不知道

我已经成功地解决了这个问题,方法是强制内联任何将与不同源文件中的不同编译器选项一起使用的模板函数。仅使用 inline 关键字通常是不够的,因为对于大于某个阈值的函数,编译器有时会忽略它,因此您必须强制编译器这样做。

在 MSVC++ 中:

template<typename T>
__forceinline int RtDouble(T number) {...}

海湾合作委员会:

template<typename T>
inline __attribute__((always_inline)) int RtDouble(T number) {...}

请记住,您可能必须强制内联 RtDouble 可能在同一模块中调用的任何其他函数,以便使编译器标志在这些函数中也保持一致。另请记住,MSVC++ 在禁用优化时(例如在调试构建中)会简单地忽略 __forceinline,在这些情况下,此技巧将不起作用,因此在非优化构建中会出现不同的行为。在任何情况下它都会使调试出现问题,但只要编译器允许内联,它确实可以工作。

我认为最简单的解决方案是让编译器知道这些函数确实是不同的,方法是使用一个除了区分它们之外什么都不做的模板参数:

文件double.h:

template<bool avx, typename T>
int RtDouble(T number)
{
    // Side effect: generates avx instructions
    const int N = 1000;
    float a[N], b[N];
    for (int n = 0; n < N; ++n)
    {
        a[n] = b[n] * b[n] * b[n];
    }    
    return number * 2;
}

文件fn_normal.cpp:

#include "fn_normal.h"
#include "double.h"

int FnNormal(int num)
{
    return RtDouble<false>(num);
}

文件fn_avx.cpp:

#include "fn_avx.h"
#include "double.h"

int FnAVX(int num)
{
    return RtDouble<true>(num);
}

编译器会生成两个对象(fn_avx.obj和fn_normal.obj),它们是用不同的指令集编译的。如您所说,输出两者的反汇编验证是否正确完成:

objdump -d fn_normal.obj:

...
movss  -0x1f5c(%ebp,%eax,4),%xmm0
mulss  -0x1f5c(%ebp,%ecx,4),%xmm0
mov    -0x1f68(%ebp),%edx
mulss  -0x1f5c(%ebp,%edx,4),%xmm0
mov    -0x1f68(%ebp),%eax
movss  %xmm0,-0xfb4(%ebp,%eax,4)
...

objdump -d fn_avx.obj:

...
vmovss -0x1f5c(%ebp,%eax,4),%xmm0
vmulss -0x1f5c(%ebp,%ecx,4),%xmm0,%xmm0
mov    -0x1f68(%ebp),%edx
vmulss -0x1f5c(%ebp,%edx,4),%xmm0,%xmm0
mov    -0x1f68(%ebp),%eax
vmovss %xmm0,-0xfb4(%ebp,%eax,4)
...

外观惊人地相似,因为默认情况下 MSVC 2013 将假定 SSE2 可用性。如果您将指令集更改为 IA32,您将获得一些非矢量指令。所以,这不是 compiler/compilation 单元的问题。

这里的问题是 RtDouble 在头文件中定义为非专用模板(完全合法)。编译器假设它在多个翻译单元中的定义是相同的,但是,通过使用不同的选项进行编译,这个假设被违反了。它与预处理器引入分歧本质上没有什么不同:

double.h:

template<typename T>
int RtDouble(T number)
{
#ifdef SUPER_BAD
// Side effect: generates avx instructions
const int N = 1000;
float a[N], b[N];
for (int n = 0; n < N; ++n)
{
    a[n] = b[n] * b[n] * b[n];
}
return number * 2;
#else
return 0;
#endif
}

fn_avx.cpp:

#include "fn_avx.h"
#define SUPER_BAD
#include "double.h"

int FnAVX(int num)
{
    return RtDouble(num);
}

FnNormal 将只是 return 0(您可以通过反汇编新的 fn_normal.obj 来验证这一点)。链接器愉快地选择一个,并且不会警告您任何一种情况。然后问题归结为:应该吗?在这种情况下,这将非常有帮助。但是,它也会减慢链接速度,因为它需要比较多个编译单元中可能存在的所有函数(例如,内联函数)。

当我在代码中遇到类似问题时,我为优化版本和非优化版本选择了不同的函数命名方案。使用模板参数来区分它们也同样有效(如@celtschk 的回答中所建议)。