为什么重新实现strlen作为循环+减法?

时间:2011-07-27 10:31:05

标签: c++ c string sqlite strlen

this question的启发,关于SQLite3中的以下代码:

 static int strlen30(const char *z){
    const char *z2 = z;
    while( *z2 ){ z2++; }
    return 0x3fffffff & (int)(z2 - z);
 }

伴随着commit message说这个函数有助于int溢出。

我对这部分特别感兴趣:

 const char *z2 = z;
 while( *z2 ){ z2++; }

对我来说,这个循环前进z2,直到z2指向null终止符。然后z2-z产生字符串长度。

为什么不对此部分使用strlen()并重写如下:

return 0x3fffffff & (int)(strlen(z));

为什么使用循环+减法代替strlen()?什么可以循环+减法做strlen()不能做什么?

2 个答案:

答案 0 :(得分:7)

我无法告诉你为什么他们必须重新实施它,以及为什么他们选择int而不是size_t作为返回类型。但关于功能:

/*
 ** Compute a string length that is limited to what can be stored in
 ** lower 30 bits of a 32-bit signed integer.
 */
static int strlen30(const char *z){
    const char *z2 = z;
    while( *z2 ){ z2++; }
    return 0x3fffffff & (int)(z2 - z);
}



关于截断,类型,溢出的标准参考

标准(ISO / IEC 14882:2003(E)) 3.9.1基本类型,4。:

  

无符号整数,声明为无符号整数,应遵守算术模2 n 的定律,其中n是该特定整数大小的值表示中的位数。 41)

     

...

     

41):这意味着无符号算术不会溢出,因为无法用结果无符号整数表示的结果   type是以模数减少的模数,该数字大于可由结果无符号整数表示的最大值   型

标准的那一部分没有定义有符号整数的溢出行为。如果我们看看 5。表达式,5。:

  

如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义,除非此类表达式是常量表达式   (5.19),在这种情况下,该程序是不正确的。 [注意:大多数现有的C ++实现忽略整数   溢出。除零处理,使用零除数和所有浮点形成余数   机器之间的例外情况各不相同,通常可以通过库函数进行调整。 ]

到目前为止溢出。

至于减去两个指向数组元素的指针, 5.7 Additive operators ,6。:

  

当减去指向同一数组对象的元素的两个指针时,结果是两个数组元素的下标的差异。结果的类型是实现定义的有符号整数类型;此类型应与标题(18.1)中定义为ptrdiff_t的类型相同。 [...]

查看 18.1

  

内容与标准C库头stddef.h

相同

那么让我们看一下C标准(我只有C99的副本), 7.17通用定义

  
      
  1. 用于size_t和ptrdiff_t的类型不应具有整数转换等级   除非实现支持对象,否则大于signed long int的值   大到足以使这成为必要。
  2.   

ptrdiff_t没有进一步的保证。然后,附件E(仍在ISO / IEC 9899:TC2中)给出了有符号long int的最小幅度,但不是最大值:

#define LONG_MAX +2147483647

现在int的最大值是sqlite - strlen30()的返回类型?让我们跳过C ++引用再次将我们转发到C标准,我们将在附件E的C99中看到int的最小最大值:

#define INT_MAX +32767



关于截断部分

的摘要
  1. 通常,ptrdiff_t不大于signed long,不小于32位。
  2. int被定义为至少16位长。
  3. 因此,减去两个指针可能会产生一个不适合您平台的int的结果。
  4. 我们从上面记得,对于签名类型,不适合的结果会产生未定义的行为。
  5. strlen30确实应用了bitwise或者指针 - 减法 - 结果:

  6.           | 32 bit                         |
    ptr_diff  |10111101111110011110111110011111| // could be even larger
    &         |00111111111111111111111111111111| // == 3FFFFFFF<sub>16</sub>
              ----------------------------------
    =         |00111101111110011110111110011111| // truncated
    

    通过将指针减法结果截断为最大值3FFFFFFF 16 = 1073741823 10 来防止不良行为。

    我不确定他们为什么选择这个值,因为在大多数机器上只有most significant bit tells the signedness。选择最小INT_MAX可能比标准更有意义,但1073741823确实有点奇怪而不知道更多细节(尽管它当然完全符合其功能上面的注释:截断到30位并防止溢出)



    “为什么不对这部分使用strlen()”

      

    并像这样重写:

    return 0x3fffffff & (int)(strlen(z));
    

    我的猜测是他们想要避免潜在的间接性。另一个优点可能是对标准库的依赖性较少,如果您编写非托管应用程序,这可能很有用。

    顺便说一句,如上所述,(int)(strlen(z))如果ptrdiff_t的最大值> 1,则可能会产生未定义的行为。 INT_MAX,所以(int)(0x3fffffff & strlen(z))会更好。

答案 1 :(得分:1)

  

为什么重新实现strlen作为循环+减法?

我怀疑真正的答案是程序员感觉如此,但另一个潜在的理由/合理化是循环是内联的(与strlen30本身是否相关),而在许多系统上strlen是一个外联函数调用(例如Linux / GCC)。如果绝大多数字符串是空的或短的(尽管对长字符串采用“特殊”处理),那么对于常见情况可能会产生轻微的性能提升。仅凭这种可能性就足以让代码开心的程序员获得密钥。对于更长的字符串,我希望库strlen通常是最优的(允许它缺乏应用程序特定字符串长度的知识)。

有些系统甚至可能无法从这种内联中受益,因为strlen提供了自己的内联,或内联/外联混合,快速内联检查空,一个字符,可能是两个字符串然后一个电话。