求最小值的宏:#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这本书所推荐的做法。

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