2015-03-31 35 views
9

我發現a question somewhat interesting,並繼續嘗試回答它。作者想要編譯一個源文件(依賴於模板庫)和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()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; 
} 


最後,我設置Enhanced Instruction SetAVX爲下的文件fn_avx.cpp「屬性 - > C/C++ - >代碼生成」,把它留給Not Set爲其他來源,因此應默認SSE2。

我認爲通過這樣做,編譯器將爲包含它的每個源實例化一次模板(並避免通過改變模板函數名稱或其他方式來違反「一維定義規則」),從而調用帶--noavx參數的程序會使其在沒有avx支持的情況下在cpus中正常運行。
但是生成的程序實際上只有一個機器代碼版本的函數,包含avx指令,並且在較老的cpus上會失敗。

禁用所有其他優化不能解決此問題。也試過No Enhanced Instructions - /arch:IA32而不是Not Set

因爲我剛剛開始瞭解模板等,有人可以指向我此行爲的具體細節以及我實際上可以做些什麼來實現我的目標?

我的編譯器是MSVC 2013年


附加信息:兩個fn_normal.cppfn_avx.cpp幾乎字節大小相同的.obj文件。我查看了生成的彙編列表,它們幾乎相同,重要的區別在於啓用avx的源分別替換默認sse的movss/mulssvmovssvmulss。但步進throught在Visual Studio中的反彙編視圖代碼(按Ctrl + Alt鍵+d),證實確實fnNormal()利用的AVX專用指令。

+1

你確定「增強指令集設置爲'AVX'」設置會影響名稱修改嗎?聽起來好像沒有,模板的名字在兩個翻譯單元中都是相同的;因爲這樣的模板定義之一被丟棄爲重複。 – 2015-03-31 23:48:40

+0

'template int RtDouble(double number)'你可以打電話給'RtDouble <0>(number)'和'RtDouble <1>(number)'應該生成單獨的函數。 – SleuthEye 2015-04-01 00:11:43

+4

我不認爲「未設置」表示「沒有增強的指令集」。 – Mehrdad 2015-04-01 00:15:59

回答

3

編譯器將生成兩個對象(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在頭文件中定義爲非專業模板(完全合法)。編譯器假定跨多個翻譯單元的定義將是相同的,但是,通過編譯不同的選項,該假設被違反。它本質上是不超過引入發散與預處理不同:

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的答案中所建議的那樣)。

+0

當你說「編譯器假設它跨多個翻譯單元的定義將是相同的」,你的意思是,_linker_? – 2015-05-20 04:02:42

2

基本上,編譯器需要最小化空間,而不是提及具有相同模板實例化2次的空間可能會導致問題,如果有靜態成員。因此,據我所知,編譯器正在爲每個源代碼處理模板,然後選擇其中一個實現,或者將實際的代碼生成推遲到鏈接時間。無論哪種方式,這是一個問題,這個AVX的東西。我最終以舊式的方式解決它 - 一些全局定義不依賴於任何模板或任何東西。對於太複雜的應用程序,這可能是一個巨大的問題。英特爾編譯器有一個最近添加的編譯指示(我不記得確切的名稱),這使得該功能在使用AVX指令後即可實施,這可以解決問題。它是多麼可靠,我不知道。

+1

我已經禁用鏈接時間代碼生成(在許多其他事情)嘗試解決它,但沒有成功。我猜你可能是對的另一種可能性。正如你所說,這種解決方案在某些情況下確實不適用。讓我們希望有人可以告訴我們究竟發生了什麼:)謝謝 – 2015-04-01 00:25:37

2

我已經成功解決了這個問題,方法是強制將與不同源文件中的不同編譯器選項一起使用的任何模板化函數內聯。只使用inline關鍵字通常是不夠的,因爲編譯器有時會忽略大於某個閾值的函數,因此您必須強制編譯器執行此操作。

在MSVC++:

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

GCC:

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

請記住,您可能要forceinline是RtDouble可以在同一模塊中,爲了調用任何其它功能,以保持編譯器標誌一致在這些功能中也是如此。另外請記住,MSVC++在禁用優化時(例如在調試版本中)會簡單地忽略__forceinline,在這種情況下,此技巧將無法使用,因此預期在未優化的版本中會出現不同的行爲。在任何情況下,它都會使調試成爲問題,但只要編譯器允許內聯,它確實可以工作。

3

我認爲最簡單的解決方法是讓編譯器知道,這些功能確實意圖是不同的,通過使用一個什麼也不做一個模板參數,但區分:

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

感謝您的回答。我不確定是否將賞金獎給你或者MuertoExcobito,因此我花了比獎金更長的時間獎勵獎勵,並且獲得了該系統的自動獎勵。對於那個很抱歉。你可以看看[我在Meta上發佈的內容](http://meta.stackoverflow。com/questions/294873/change-auto-award-bounty-after-grace-period)。 – 2015-05-20 21:59:01