【问题标题】:How to conditionally set compiler optimization for template headers如何有条件地为模板头设置编译器优化
【发布时间】:2015-06-05 11:35:21
【问题描述】:

我找到了a question somewhat interesting,并试图回答它。作者想用 AVX 优化编译-one-source 文件(依赖于模板库),而项目的其余部分没有这些。

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

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()FnAVX()的声明,定义如下:

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.cppEnhanced Instruction Set 设置为 AVX,将其他源设置为 Not Set,因此它应该默认为 SSE2。

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

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

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

我的编译器是 MSVC 2013。


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

【问题讨论】:

  • 您确定“增强指令集为'AVX'”设置会影响名称修改吗?听起来好像没有,并且模板名称的修改在两个翻译单元中是相同的;因此,模板的定义之一被丢弃为重复项。
  • template&lt;int option&gt; int RtDouble(double number) 您将调用为 RtDouble&lt;0&gt;(number)RtDouble&lt;1&gt;(number) 应该生成单独的函数。
  • 我不认为“未设置”意味着“没有增强的指令集”。
  • @SamVarshavchik 确实是这样。我添加了新的细节,请看一下。
  • 那么做两个不同的函数:coliru.stacked-crooked.com/a/41f45dd089100bc6

标签: c++ templates visual-c++ compiler-optimization


【解决方案1】:

编译器会生成两个对象(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,您将获得非向量指令。所以,这不是编译器/编译单元的问题。

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

双.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 的回答中所建议的那样)。

【讨论】:

  • 当您说“编译器假定其跨多个翻译单元的定义将是相同的”时,您的意思是 链接器
【解决方案2】:

基本上,编译器需要最小化空间,更不用说将相同的模板实例化 2x 如果存在静态成员可能会导致问题。因此,据我所知,编译器正在为每个源代码处理模板,然后选择其中一个实现,或者将实际代码生成推迟到链接时间。无论哪种方式,这对这个 AVX 东西来说都是一个问题。我最终以老式的方式解决了它 - 一些全局定义不依赖于任何模板或任何东西。但是,对于过于复杂的应用程序,这可能是一个巨大的问题。英特尔编译器最近添加了一个编译指示(我不记得确切的名称),它使函数在仅使用 AVX 指令后立即实现,这将解决问题。它有多可靠,我不知道。

【讨论】:

  • 我在尝试解决它时禁用了链接时间代码生成(以及许多其他功能),但没有成功。我猜你可能是对的另一种可能性。正如您所说,该解决方案在某些情况下确实不合适......希望有人能告诉我们到底发生了什么:) 谢谢
【解决方案3】:

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

在 MSVC++ 中:

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

海合会:

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

请记住,您可能必须强制内联 RtDouble 可能在同一模块中调用的任何其他函数,以使编译器标志在这些函数中也保持一致。还要记住,当优化被禁用时,MSVC++ 会简单地忽略 __forceinline,例如在调试版本中,在这种情况下,这个技巧将不起作用,所以在非优化的版本中会有不同的行为。在任何情况下它都可能使调试产生问题,但只要编译器允许内联,它确实可以工作。

【讨论】:

    【解决方案4】:

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

    文件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);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-02-26
      • 2010-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-11
      • 1970-01-01
      相关资源
      最近更新 更多