求最小值的宏:①#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這本書所推薦的做法。