求最小值的宏:#define min(x,y) x > y? y: x 中的陷阱,慎用

求最小值的宏:①#define min(x,y)     x > y? y: x。這個宏網上遍地都是,殊不知,這個宏存在嚴重bug。

順便再列一下,下面這幾個宏也存在嚴重bug,使用前一定要仔細考慮

②#define min(x,y)         (x) > (y)? (y): (x)
③#define min(x,y)         ((x) > (y)? (y): (x))

上面這幾個宏在大多數條件下都能正常工作,在某些特殊條件或者應用場景下才會發生bug,所以並不是說這幾個宏不能用,而是要慎用,一定要考慮清楚它的應用場景。

 

文章最後我們會看一下,linux內核是如何寫min宏的。

 

先不提這幾個宏的bug,先看看這幾個宏的效率:

int result = min(a,   b* 2 + c);就這一句中,求min的過程中,上面幾個宏都會把表達式:b* 2 + c計算兩遍,純屬浪費CPU資源。

 

浪費CPU資源還算是小事,下面我們來看看以上這幾個宏是如何導致bug的,這纔是致命的:
(1)bug類型1:表達式做實參被執行兩次

  int result = min(a, b++);這裏b++會被執行兩次,返回不符合你期望的一個結果。

(2)bug類型2:

來看看宏①的bug:   int res = 5 * min(2 , 3);計算結果竟然爲2,原因就在於它被展開爲: res = 5 * 2 > 3? 2: 3;

(3)

看起來宏②的bug只要用宏③就能解決,實際上宏③也有bug,宏③除了表達式求值兩次的bug外,還有別的陷阱。

uint16 b = 1, c = 2, d = 3;
uint16_t len = min(b, c - d);

上面這行,不論是返回值,還是實參,都是u16類型,由於第一實參b = 1,感覺上只要第二實參y不是0,那麼min(1, y)就應該返回1纔對,實際返回的len = 65535。

也許很多朋友都能一眼看出這裏存在bug,那是因爲bcd的賦值都寫在上面了,你知道y = -1 = 65535,如果這行代碼出現在函數中,你不一定能發現這個陷阱,例如linux內核中的kfifo_get函數:
如果你使用上面的3個劣質min宏之一,那麼kfifo_get這個函數將無法正常工作,

 

//#define min(x,y)         ((x) > (y)? (y): (x))   // bug宏

//如果使用了上面這個宏,那麼下面這個函數將運行出錯

unsigned int __kfifo_get(struct kfifo *fifo,
             unsigned char *buffer, unsigned int len)
{
    unsigned int l;
 
    len = min(len, fifo->in - fifo->out);
 
    /*
     * Ensure that we sample the fifo->in index -before- we
     * start removing bytes from the kfifo.
     */
 
    smp_rmb();
 
    /* first get the data from fifo->out until the end of the buffer */
    l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));
    memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);
 
    /* then get the rest (if any) from the beginning of the buffer */
    memcpy(buffer + l, fifo->buffer, len - l);
 
    /*
     * Ensure that we remove the bytes from the kfifo -before-
     * we update the fifo->out index.
     */
 
    smp_mb();
 
    fifo->out += len;
 
    return len;
}

假設我們有一個環形緩衝(fifo)區char buf[256],還有個寫緩衝索引: u16 in(指向可用的空位置),讀緩衝索引:u16 out(指向最後一個讀完的位置),那麼根據unsigned類型的環回特性,fifo中的已用空間總是=in - out,例如in = 8,out=3時,已用空間=8-3 = 5,即使in發生了環回,這個等式依然成立,例如:in = 1,out = 65535時,已用空間爲2,而u16的運算時:1 - 65535 = 2。根據這一環回邏輯:in - out總是代表了已用空間的大小,它介於[0~size]之間,是不是感覺(in - out)恆爲非負?!!!
那麼計算min(1, in - out)時,是不是隻要(in - out)不是0,min就應該返回1!bug就在這裏產生了。

原因就在於,1-65535=2,這個條件只有在返回值仍爲u16時,才成立,直接寫1-65535得出的結果並不是2,而是-65534,那麼min(1, in - out)就等價於min(1, -65535),min的返回值賦值給u16 len時,就變成了len = -65534 = 2,而我們的本意卻是len = min(1, 已用空間=2) = 1。這就是kfifo中的min陷阱。

關於unsigned的環回特性的巧妙應用,請自行搜索《linux內核kfifo》學習,或者看我的另一篇文章《

利用整數的環回特性打造高效計時器、補碼反碼、負數的內存佈局

對於kfifo中min宏的改進,可以把min寫成#define min(x, y)     ((uint)(x) > (uint)(y)? (uint)(x): (uint)(y)),這個宏可以避開環回特性應用的bug,當然,表達式做實參時,會被計算兩次的這個bug還在。

 

 

 

下面是linux內核中定義的min宏:

#define min(x,y) ({ \
    typeof(x) _x = (x);    \
    typeof(y) _y = (y);    \
    (void) (&_x == &_y);    \
    _x < _y ? _x : _y; })

看起來有些複雜,實際上精妙無比,無論從效率,還是安全性上,都無可挑剔:

(1)首先把形參裏的x和y,都求出來存到_x和_y裏面,這樣就避免了:形參爲表達式時,需要求兩次表達式值的弊端。
有朋友可能會想,如果形參不是表達式而就是個單變量的話,用這個宏豈不是降低了效率,實際上並不會,除非你指定編譯器使用O(0)優化,否則,編譯器會優化爲最高效的代碼的,看一下編譯出的彙編代碼即可證明這一點。

(2)  (void) (&_x == &_y)這一句乍看是一句廢話,沒有任何作用,其實不然。這一句是爲了檢查x和y的類型是否一致(C語言並沒有類型檢查功能,所以這裏巧妙地用取地址,對比指針的方式來產生警告),如果不一致,這一行會發生編譯警告,以便提示程序員注意。這裏(void)的作用是強制執行本行代碼,否則這行代碼在編譯器看來確實是一句廢話,會被優化掉。關於這行代碼的精妙之處,可以自行搜索“(void) (&_x == &_y)”,網上有很多更詳細的解釋。

不足之處:typeof關鍵字只在GUN編譯器支持,標準C89、C99都不支持。

總結:建議用內聯函數,除非你對應用min的場景陷阱非常熟悉,否則還是用內聯函數比較穩妥,這也是effective c這本書所推薦的做法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章