2009年12月5日 星期六

Undefine Behavior 和 GCC

有個編譯器的測試程式是這樣寫的,基本上這些測試程式會利用執行程式的回傳值來判斷編譯器是否能通過測試。換句話說,只要編譯並執行了這個測試程式而且執行程式的回傳值是0,就代表編譯器通過這個測驗,反之,就代表編譯器無法通過這個測驗。( 註: 呼叫abort將回傳1,也就是代表測試失敗。)
void f(int i)
{
  i = i > 0 ? i : -i;   if (i<0)
    return;
  abort ();
}
int main(int argc, char *argv[])
{
  f(INT_MIN);
  exit (0);
}
上述程式在進行的其實就是 abs (absolute value) 的測試,看起來如此簡單的一個程式,長的雖然如此甜美,可是卻是包藏禍心的。跟電腦上的數值表示法熟的人就會發現其實有某個數字是沒有辦法正確的做"數學上的" absolute value,也不難猜的,他就是那個整數的最小值。對整數的最小值來說,他並沒有對應的正數值,所以整數的最小值進行絕對值運算後會溢位。以在ISO C99 spec 當中,對整數的絕對值相關函式的解釋來說,這樣的行為將會是未定義的。既然是未定義的,就會取決於硬體的實作。一般而言,硬體實作絕對值時,通常會有兩種實作方式,第一種就是直接運算,也就是整數的最小時做絕對值後的結果還會是他原本的值(以32位元來說0xf0000000取補數+1還是0xf0000000),第二種狀況在DSP比較常出現,就是硬體會幫忙做飽和運算,也就是結果會是正的最大值(0x7fffffff)。
編譯器的核心是IR(intermediate representation),IR對一個編譯器進行最佳化時的重要程度,是可以用"IR是編譯器的生命"來形容的。 上述程式粗體表示的部分,其實出乎大部分人(?)意料之外的,如果硬體有支援ABS指令的話,大部分的主流編譯器是能產生ABS指令來最佳化這個運算式的。簡單來說,GCC 為了進行最佳化,定義了許多低階的指令範本(Instruction Pattern),用來match C 語言所代表的運算式,當然中間還兼過了許多跟平台無關的最佳化過程(像Loop Optimization),如果目標平台的硬體有定義abs的Instruction Pattern,GCC就會拿它來match上面那個運算式,也就是為什麼GCC能選擇性的用上abs指令來最佳化那條運算式。不過就一個未定義的行為而言,有必要去測試它的正確性嗎?
很可惜的是GCC是支援多個程式語言的前端,這個未定義的表示式在某些語言中是有定義(對,我就是在說你,Java)。JLS (Java Langauge Sepc) 中定義了有號算數運算如果有溢位的話,必須要使用2的補數表示法來 wraps around 。也就是說,在Java中,剛剛那個絕對值運算"必須"也應該要維持原本的值。GCC為了同時支援兩種不相容的Spec,所以就定義了一個編譯器選項-fwrapv來指定編譯器要不要wraps around有號運算的結果。所以上面那支測式程式還是有它存在的意義,其實編譯器在編譯這個程式時,就是要測試-fwrapv這個編譯器選項有沒有實作正確,這對一些只有 saturates 版本ABS指令的CPU/DSP/GPU造成一些困擾,也就是說當-fwrapv這個選項打開的時候,編譯器要清楚的知道不能使用abs的Instruction Pattern去match那個運算式了。

沒有留言:

張貼留言