2016-07-07 53 views
18

Chandler Carruth在他的CppCon2015 talk中引入了兩個函數,可用於對優化器進行細粒度抑制。他們寫微觀基準是非常有用的,優化器不會簡單地導致無意義。MSVC中microbenchmarks的優化障礙:告訴優化器你有clobber內存?

void clobber() { 
    asm volatile("" : : : "memory"); 
} 

void escape(void* p) { 
    asm volatile("" : : "g"(p) : "memory"); 
}  

這些使用內聯彙編語句來更改優化器的假設。

clobber中的彙編語句聲明其中的彙編代碼可以讀寫內存中的任何位置。實際彙編代碼是空的,但優化器不會查看它,因爲它是asm volatile。它相信,當我們告訴它代碼可能在內存中的任何地方都可以讀寫。這有效地防止優化器在調用clobber之前重新排序或丟棄內存寫入,並在調用clobber†之後強制讀取內存。

escape中的那個,另外使指針p對組裝塊可見。同樣,由於優化器不會查看實際的內聯彙編代碼,因此代碼可能爲空,並且優化器仍然會假定該塊使用指針p指向的地址。這有效地強制任何p點在內存中,而不是在寄存器中,因爲程序集塊可能執行從該地址讀取。 (這很重要,因爲clobber函數不會強制讀取或寫入編譯器決定放入寄存器的任何內容,因爲clobber中的彙編語句沒有聲明任何特別的內容必須對於組裝)。

所有這些都是在沒有任何其他代碼直接由這些「障礙」生成的情況下發生的。它們純粹是編譯時的工件。

這些使用GCC和Clang支持的語言擴展。有沒有辦法在使用MSVC時有類似的行爲?


†爲了理解優化器爲什麼要這樣想,想象一下,如果程序集塊是一個循環,爲內存中的每個字節加1。

+1

它看起來[象](http://stackoverflow.com/ a/8845503/786653)'_ReadWriteBarrier'可能是'clobber'的答案。雖然我不知道「逃跑」。也許'_ReadWriteBarrier'加上指針指向一些外部定義的函數。 – user786653

+0

哦,我忘了提及這些的另一個特點:它們不生成任何代碼。優化器完成後,它們消失的任何效果都會消失。直到運行時纔會有什麼他們純粹是編譯時。 –

+0

像@ user786653說的那樣,'_ReadWriteBarrier'(或者也許只是'_ReadBarrier' /'_WriteBarrier'如果這就是所需要的)在MSVC中會和'clobber'有相同的效果。對於'逃避',我在分析程序集輸出方面的經驗是,如果你只是標記變量'volatile',MSVC會做正確的事情。當然,在這裏有一些運行時間的開銷,因爲生成的代碼將*總是*保持變量在內存中更新。這不是一個完美的解決方案,但我還沒有找到更好的解決方案。 –

回答

1

我用以下來代替escape

#ifdef _MSC_VER 
#pragma optimize("", off) 
template <typename T> 
inline void escape(T* p) { 
    *reinterpret_cast<char volatile*>(p) = 
     *reinterpret_cast<char const volatile*>(p); // thanks, @milleniumbug 
} 
#pragma optimize("", on) 
#endif 

這不是完美的,但它足夠接近我想。

不幸的是,我沒有辦法效仿clobber

6

鑑於your approximation of escape(),你也應該罰款與clobber()以下近似(注意,這是一個想法草案,推遲了一些解決方案的功能nextLocationToClobber()的實現):

// always returns false, but in an undeducible way 
bool isClobberingEnabled(); 

// The challenge is to implement this function in a way, 
// that will make even the smartest optimizer believe that 
// it can deliver a valid pointer pointing anywhere in the heap, 
// stack or the static memory. 
volatile char* nextLocationToClobber(); 

const bool clobberingIsEnabled = isClobberingEnabled(); 
volatile char* clobberingPtr; 

inline void clobber() { 
    if (clobberingIsEnabled) { 
     // This will never be executed, but the compiler 
     // cannot know about it. 
     clobberingPtr = nextLocationToClobber(); 
     *clobberingPtr = *clobberingPtr; 
    } 
} 

UPDATE

問題:你將如何保證isClobberingEnabled返回false「以不可誘導的方式」?當然,將定義放在另一個翻譯單元中會很簡單,但是在啓用LTCG的那一刻,該策略就會失敗。你有什麼想法?

:我們可以採取一個難以證明從數論性質,例如,Fermat's Last Theorem的優勢:

bool undeducible_false() { 
    // It took mathematicians more than 3 centuries to prove Fermat's 
    // last theorem in its most general form. Hardly that knowledge 
    // has been put into compilers (or the compiler will try hard 
    // enough to check all one million possible combinations below). 

    // Caveat: avoid integer overflow (Fermat's theorem 
    //   doesn't hold for modulo arithmetic) 
    std::uint32_t a = std::clock() % 100 + 1; 
    std::uint32_t b = std::rand() % 100 + 1; 
    std::uint32_t c = reinterpret_cast<std::uintptr_t>(&a) % 100 + 1; 

    return a*a*a + b*b*b == c*c*c; 
} 
+0

@Peter注意到'isClobberingEnabled'只被調用一次(它用於名稱空間範圍)。但是,也許你的觀點仍然適用於'nextLocationToClobber'。 –

+1

@ R.MartinhoFernandes:剛注意到並刪除了我的評論。重新發佈一個正確的版本:'調用'到'nextLocationToClobber'意味着編譯器不能將包含它的函數當作葉函數。希望溢出的呼叫限制寄存器只限於發生呼叫的分支,對未採取的一方不會有太大的影響,但它仍然是非零影響。它至少會編譯成一個全球測試和分支。因此,與gcc不同,生成的代碼量非零。 :/仍然,一個可預測的分支是便宜的。 –

+0

這可能是你可以用MSVC做的最好的,但是如果它沒有任何可以幫助的內建/內部函數,那將是令人失望的。 –