优化在编译时应该知道的if-else,但是信息在不同的项目中

时间:2019-01-14 05:44:20

标签: c++ c++14 constexpr template-specialization

如何使用户能够定义模板功能doIt(a,b)将从另一个用户定义的变量/函数op出发的“行为模式”,并优化 if-else ?

这是正在运行的MCVE。
我的VS解决方案有2个项目。 pj1 是我的图书馆。 pj2 是用户的项目。

A_pj1.h

#pragma once
template<int T>class BoolT{public:
    static bool op;
};
template<int T> bool BoolT<T>::op=true; //by default,  true=+, false=-
template<int i> int doIt(int a,int b){
    if(BoolT<i>::op){
        return a+b;
    }else{ return a-b;}
}

A_pj2_UserDefine.h

#pragma once
#include "A_pj1.h"
inline void A_pj2_UserDefine_Reg(){
    BoolT<2>::op=false; //override default value;
}

A_pj2_main.cpp

#include "A_pj2_UserDefine.h"
#include <iostream>
int main(){
    A_pj2_UserDefine_Reg();
    int s1=doIt<1>(3,2); //= 5 (correct)
    int s2=doIt<2>(3,2); //= 1 (correct)
    std::cout << s1<<" "<<" "<<s2<<std::endl;
    int asfasd=0;
}

(编辑)这是反汇编(优化版本):-

int s2=doIt<2>(3,2);
    std::cout << s1<<" "<<" "<<s2<<std::endl;
00B61620  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0B6B0A8h)]  
    int s1=doIt<1>(3,2); //:
00B61626  xor         eax,eax  
00B61628  cmp         byte ptr [BoolT<1>::op (0B6E001h)],al  
    int s2=doIt<2>(3,2);
    std::cout << s1<<" "<<" "<<s2<<std::endl;
00B6162E  push        offset std::endl<char,std::char_traits<char> > (0B61530h)  
00B61633  push        1  
    int s1=doIt<1>(3,2); //:
00B61635  setne       al  
    A_pj2_UserDefine_Reg();
00B61638  mov         byte ptr [BoolT<2>::op (0B6E000h)],0  
    int s2=doIt<2>(3,2);
    std::cout << s1<<" "<<" "<<s2<<std::endl;
00B6163F  push        offset string " " (0B6B220h)  
00B61644  push        offset string " " (0B6B220h)  
    int s1=doIt<1>(3,2); //:
00B61649  lea         eax,[eax*4+1]  
    int s2=doIt<2>(3,2);
    std::cout << s1<<" "<<" "<<s2<<std::endl;
00B61650  push        eax  
00B61651  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0B6B0B4h)]  
00B61657  push        eax  
00B61658  call        std::operator<<<std::char_traits<char> > (0B61310h)  
00B6165D  add         esp,8  
00B61660  push        eax  
00B61661  call        std::operator<<<std::char_traits<char> > (0B61310h)  
00B61666  add         esp,8  
00B61669  mov         ecx,eax  
00B6166B  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0B6B0B4h)]  
00B61671  mov         ecx,eax  
00B61673  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0B6B0B8h)]  
    int asfasd=0;
}
00B61679  xor         eax,eax  
00B6167B  ret  

问题

doIt(a,b)经常被调用(每秒> 60000)。
我很好奇if(BoolT<i>::op)是否可以优化。
如果条件不适合管道计算。

这是我的程序行为,可能会有所帮助:-

  • BoolT<2>::op的访问-写入始终仅发生在程序的开头(例如A_pj2_UserDefine_Reg())。
  • 功能查询BoolT<2>::op(直接和间接)几乎总是只在项目pj2中发生。
  • 用户无法编辑 pj1
  • 在实际情况下,除了A_pj2_main.cpp外,还有许多.cpp调用pj1的{​​{1}}。这是包含图:-

enter image description here

我可怜的解决方案

B_pj1.h

doIt<>()

B_pj2_UserDefine.cpp

#pragma once
template<int i> bool op(){return true;}
template<int i> int doIt(int a,int b){
    if(op<i>()){
        return a+b;
    }else{ return a-b;}
    // return a+b;
}

B_pj2_main.cpp

#include "B_pj1.h"
template<> bool op<2>(){return false;}

该程序格式错误。 (参考:Storing C++ template function definitions in a .CPP file

  

错误LNK2005“ bool __cdecl op <2>(void)”(?? $ op @ $ 01 @@ YA_NXZ)已经   在B_pj2_UserDefine.obj

中定义

2 个答案:

答案 0 :(得分:1)

您当前的代码很好。如果条件每次都采用不同的分支,则仅会影响管线计算。当该标志仅写入一次时,分支预测会做得很好。

无论如何,常见的解决方案是C预处理器宏。从技术上讲可以解决if constexpr,但实际上这需要库#include从用户项目中获取某些东西,并且大多数库作者都不希望支持这种用例。

如果您愿意只使用库标头,也可以使用模板来解决,但更为复杂。由于以下原因,我尝试尽可能避免跨库的模板。编译时间开销。

答案 1 :(得分:0)

我认为示例被简化得太多,以至于看不到意图。因此,让我假设您有充分的理由这样做。

首先,请确保您在MSVC上进行了优化,将在命令行中添加/ O2(或类似名称)。

第二,测量。 CPU中的分支预测变量确实非常有效,您甚至可能不会注意到。

也就是说,当编译器看到if (false)并且没有对其进行优化时,您可以认为它是编译器中的错误。不断折叠,尤其是在使用SSA作为模型时,是如此琐碎,以至于那些分支应该很快消失。

如果有疑问或想要在编译时强制使用,则使用if constexpr是解决方案。它确实需要C ++ 17,并且可以在MSVC2017中使用。

另一种选择是使函数constexpr,尽管看起来像这样可能并不总是可能的:

template<int i> constexpr int doIt(int a,int b){
    if(BoolT<i>::op){
        return a+b;
    }else{ return a-b;}
}

您可以这样称呼它:

constexpr int s1=doIt<1>(3,2);
int s2=doIt<2>(3,i);

这样,您的编译器需要在编译时计算s1的值。尽管它可以在运行时重用该函数来计算s2。 (请注意我正在传递变量)

关于链接器错误,C ++中有一个ODR(一个定义规则)。您可以通过添加内联来解决它,但是,到处都应该有相同的实现!我不建议将实现放在CPP文件中,因为它是UB的收据。