2011-07-29 54 views
6

我一直在使用gcc constpure屬性來返回指向第一次使用時分配和初始化的「常量」數據的函數,即函數將每次調用時返回相同的值。作爲一個例子(不是我的用例,但是一個衆所周知的例子)想到了一個函數,它在第一次調用時分配和計算trig查找表,並且只是在第一次調用之後返回一個指向現有表的指針。初次使用函數的gcc屬性

的問題:有人告訴我這個用法不正確,因爲這些屬性禁止副作用,編譯器甚至可以完全優化了通話在某些情況下,如果不使用返回值。是我的const/pure使用屬性安全,或有任何其他的方式來告訴編譯器N>1調用該函數相當於1次調用該函數,但1次調用該函數不等同於0來電來功能?或者換句話說,函數第一次被調用時只有副作用?

+1

你確定這是個問題嗎?如果呼叫被優化了,那麼下次會創建數據。只要數據只能通過返回值訪問,這應該沒有問題。 – ughoavgfhw

+0

'pure'是GCC特有的,但'const'不是。 –

+0

'__attribute __((const))'也是一個gcc-ism,但非gcc編譯器更廣泛地支持... –

回答

6

我說這是正確的基礎上我的常量理解,但如果任何人有兩個的精確定義,請說出來。這得到棘手,因爲海灣合作委員會的文件沒有規定確切意味着什麼功能有「除返回值沒有影響」(用於)或「不審覈除它們的參數的任何值」(對於常量)。顯然,所有功能都一些效果(它們使用處理器週期,存儲器修改)並檢查一些值(功能代碼,常數)。

「副作用」必須根據C編程語言的語義來定義,但我們可以根據這些屬性的目的來猜測GCC人員的意思,即啓用其他優化(至少這就是我所假設的)。

原諒我,如果下面的一些過於基本...

純函數可以參與公共子表達式消除。他們的特點是他們不修改環境,所以編譯器可以自由地調用更少的時間而不改變程序的語義。

z = f(x); 
y = f(x); 

變爲:

z = y = f(x); 

或獲取完全消除,如果zy未使用。

所以我最好的猜測是,「純」的工作定義爲「它可以在不改變程序的語義來調用次數的任何功能」。然而,函數調用可以不移動,例如,

size_t l = strlen(str); // strlen is pure 
*some_ptr = '\0'; 
// Obviously, strlen can't be moved here... 

const函數可以重新排序,因爲它們不依賴於動態環境。

// Assuming x and y not aliased, sin can be moved anywhere 
*some_ptr = '\0'; 
double y = sin(x); 
*other_ptr = '\0'; 

所以我最好的猜測是「常量」的工作定義爲「可以在任何時候被調用,而不改變程序的語義的任何功能」。然而,有一種危險:

__attribute__((const)) 
double big_math_func(double x, double theta, double iota) 
{ 
    static double table[512]; 
    static bool initted = false; 
    if (!initted) { 
     ... 
     initted = true; 
    } 
    ... 
    return result; 
} 

因爲它是常量,編譯器可能重新排序...

pthread_mutex_lock(&mutex); 
... 
z = big_math_func(x, theta, iota); 
... 
pthread_mutex_unlock(&mutex); 
// big_math_func might go here, if the compiler wants to 

在這種情況下,它可以同時從兩個處理器叫做即使只出現這在代碼中的關鍵部分內。然後,處理器可能會決定推遲更改table,更改爲initted已經過了,這是個壞消息。你可以用內存障礙或pthread_once來解決這個問題。

我不認爲這個bug會在x86上出現,我不認爲它出現在許多沒有多個物理處理器(不是核心)的系統上。所以它可以很好地工作很長時間,然後在雙插槽POWER計算機上突然失效。

結論:這些定義的優點是,他們說清楚編譯器允許什麼樣的變化在這些屬性,(我認爲)在GCC文檔有些模糊的情況下作出。缺點是不清楚這些是GCC團隊使用的定義。

例如,如果您看一下Haskell語言規範,您會發現純度更準確的定義,因爲純度對於Haskell語言非常重要。

編輯:我一直無法強迫GCC或鏘成移動跨越另一個函數調用一個孤__attribute__((const))函數調用,但似乎完全有可能在未來,這樣的事情會發生。記得-fstrict-aliasing成爲默認時,每個人突然在他們的程序中有更多的錯誤? 這就是那種讓我謹慎的東西。

在我看來,當你標記功能__attribute__((const)),你是有希望的編譯器,函數調用的結果是相同的,無論當你的程序的執行過程中被調用時,只要參數是相同。

但是,我確實想出了一種將const函數移出臨界區的方法,儘管我這樣做的方式可能被稱爲「作弊」。

__attribute__((const)) 
extern int const_func(int x); 

int func(int x) 
{ 
    int y1, y2; 
    y1 = const_func(x); 
    pthread_mutex_lock(&mutex); 
    y2 = const_func(x); 
    pthread_mutex_unlock(&mutex); 
    return y1 + y2; 
} 

編譯器把這個成以下代碼(從組件):

int func(int x) 
{ 
    int y; 
    y = const_func(x); 
    pthread_mutex_lock(&mutex); 
    pthread_mutex_unlock(&mutex); 
    return y * 2; 
} 

請注意,這不會只__attribute__((pure))發生,const屬性,只有const屬性觸發該行爲。如你所見,臨界區內的呼叫消失了。看起來相當武斷,先前的調用被保留了下來,而且我也不願意承認編譯器在未來的某個版本中不會做出關於保留哪個調用或是否可能將函數調用移到某處的不同決定其他完全。

結論2:仔細輪距,因爲如果你不知道你正在編譯器什麼承諾,編譯器的未來版本可能會讓你大吃一驚。

+0

在你的關鍵部分的例子中,不會有內存屏障(通常是互斥體的實現)在'unlock'調用中使用存儲釋放語義防止將調用重定位到臨界區內部到臨界區外部的'big_math_func'? – Jason

+0

@Jason:內存屏障只能防止處理器(而不是編譯器)對某些操作進行重新排序。爲了比較,'volatile'關鍵字可以防止編譯器(而不是處理器)對某些操作進行重新排序。當你爲多處理器系統編寫代碼時(特別是在低級別),它有助於將處理器和編譯器視爲你的敵人。 –

+0

對於任何可以從其他線程訪問的數據,基本上任何數據都可以訪問互斥鎖,它也是一個完整的編譯器屏障,也就是除了本地變量,其地址您從未採取或至少從未傳遞到外部世界。 –