【问题标题】:Why reimplement strlen as loop+subtraction?为什么将 strlen 重新实现为循环+减法?
【发布时间】:2011-10-14 03:31:36
【问题描述】:

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 指向空终止符。然后z2-z 产生字符串长度。

为什么不在这部分使用strlen() 并像这样重写:

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

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

【问题讨论】:

  • 我看到了另一个问题。看起来像是 NIH 综合症 (en.wikipedia.org/wiki/Not_Invented_Here),但我希望有更好的理由。
  • 我真的不明白为什么这是近距离投票。我不问strlen30() 的意图是什么——这个函数做了一些额外的事情,我只问为什么要重新实现strlen()
  • @0A0D:对我来说,循环将在非终止字符串上失败,就像strlen() 一样困难。
  • 这段代码本身仍然存在与溢出相关的问题。 ptrdiff_t 可能大于int,在这种情况下,该值可能超出int 的范围。从技术上讲不是“溢出”,因为该术语严格意味着在int 算术期间超出int 的范围并且是UB,但转换的结果是实现定义的,因此它不一定很有用。因此,要么代码仍然存在缺陷,要么在庞大的 sqlite 代码或文档中的某处对实现行为有一些额外的假设......
  • 可能注释的意思不是这个函数永远不会溢出,而是调用者对结果所做的事情永远不会溢出。剪辑到 30 位具有您可以执行 strlen30(a) + strlen30(b) 的属性,结果适合 32 位签名 int 而不会溢出。该结果可能完全没有意义,因为在 strlen30 中截断了一个值,但该添加(例如字符串连接)不会溢出。

标签: c++ c string sqlite strlen


【解决方案1】:

我不能告诉你为什么他们必须重新实现它,以及为什么他们选择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.:

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

...

41):这意味着无符号算术不会溢出,因为结果不能由生成的无符号整数表示 type 以比得到的无符号整数可以表示的最大值大一的数字为模减少 输入

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

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

到目前为止溢出。

至于两个指向数组元素的指针相减,5.7 加法运算符,6.:

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

18.1

内容同标准C库头文件stddef.h

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

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

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

#define LONG_MAX +2147483647

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

#define INT_MAX +32767



关于截断部分的总结

  1. 通常ptrdiff_t不大于signed long,不小于32bits。
  2. int 被定义为至少 16 位长。
  3. 因此,减去两个指针可能会得到不适合您平台的int 的结果。
  4. 我们从上面记得,对于有符号类型,不适合的结果会产生未定义的行为。
  5. strlen30 确实按位或​​在指针减结果上应用:

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

通过将指针减法结果截断为最大值 3FFFFFFF16 = 107374182310 来防止未定义行为。

我不确定他们为什么选择这个值,因为在大多数机器上,只有most significant bit tells the signedness。与标准相比,选择最小值INT_MAX 可能是有意义的,但在不了解更多细节的情况下,1073741823 确实有点奇怪(尽管它当然完美地完成了他们函数上方的评论所说的:截断到 30 位并防止溢出)。



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

并像这样重写它:

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

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

顺便说一句,从上面的参考资料中可以看出,如果 ptrdiff_t > INT_MAX 的最大值,(int)(strlen(z)) 可能会产生未定义的行为,因此(int)(0x3fffffff &amp; strlen(z)) 会更好。

【讨论】:

  • 你为什么没有在那个问题中回答这个问题?我不明白它是如何解释循环的。
  • 糟糕,抱歉,我以为您会对截断内容感兴趣。我应该读得更好。我将投票重新开放并考虑回答循环问题。
  • @sharptooth:完成。或者更好:尝试过。
【解决方案2】:

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

我怀疑真正的答案是程序员喜欢它,但另一个潜在的理由/合理化是循环是内联的(独立于 strlen30 本身是否是),而在许多系统上 strlen 是一个输出-离线函数调用(例如 Linux/GCC)。如果绝大多数字符串是空的或短的(尽管对长字符串进行了“特殊”处理),那么对于常见情况可能会产生轻微的性能提升。仅这种可能性就足以让喜欢代码的程序员敲键。对于更长的字符串,我希望库 strlen 通常是最佳的(考虑到它对应用程序特定的字符串长度缺乏了解)。

某些系统甚至可能无法从这种内联中受益,因为strlen 提供了它自己的,或者是内联/外联混合,可以快速内联检查空的、单字符、可能是双字符的字符串,然后调用.

【讨论】:

    猜你喜欢
    • 2014-08-05
    • 2012-09-27
    • 1970-01-01
    • 2013-10-14
    • 2013-12-17
    • 2018-11-12
    • 2012-07-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多