【问题标题】:Why does this implementation of strlen() work?为什么 strlen() 的这种实现有效?
【发布时间】:2013-12-17 05:15:42
【问题描述】:

(免责声明:我已经看过this question,我不会再问了——我对为什么代码有效,而不感兴趣em>如何工作。)

所以here's 这是 Apple(好吧,FreeBSD 的)strlen() 的实现。它使用了一个众所周知的优化技巧,即一次检查 4 或 8 个字节,而不是与 0 进行逐字节比较:

size_t strlen(const char *str)
{
    const char *p;
    const unsigned long *lp;

    /* Skip the first few bytes until we have an aligned p */
    for (p = str; (uintptr_t)p & LONGPTR_MASK; p++)
        if (*p == '\0')
            return (p - str);

    /* Scan the rest of the string using word sized operation */
    for (lp = (const unsigned long *)p; ; lp++)
        if ((*lp - mask01) & mask80) {
        p = (const char *)(lp);
        testbyte(0);
        testbyte(1);
        testbyte(2);
        testbyte(3);
#if (LONG_BIT >= 64)
        testbyte(4);
        testbyte(5);
        testbyte(6);
        testbyte(7);
#endif
    }

    /* NOTREACHED */
    return (0);
}

现在我的问题是:也许我错过了明显的,但这不能读到字符串的末尾吗?如果我们有一个长度不能被字长整除的字符串怎么办?想象以下场景:

|<---------------- all your memories are belong to us --------------->|<-- not our memory -->
+-------------+-------------+-------------+-------------+-------------+ - -
|     'A'     |     'B'     |     'C'     |     'D'     |      0      |
+-------------+-------------+-------------+-------------+-------------+ - -
^                                                      ^^
|                                                      ||
+------------------------------------------------------++-------------- - -
                       long word #1                      long word #2

当读取第二个长字时,程序访问了它实际上不应该访问的字节......这不是错了吗?我非常有信心 Apple 和 BSD 人员知道他们在做什么,所以有人可以解释一下为什么这是正确的吗?

我注意到的一件事是beerboy asserted this to be undefined behavior,我也相信确实如此,但有人告诉他事实并非如此,因为“我们将单词大小与初始 for 循环对齐”(此处未显示)。但是,如果数组不够长并且我们正在读取它的末尾,我根本不明白为什么对齐会有任何相关性。

【问题讨论】:

  • 注意:除了潜在的 UB 得到很好的回答之外,该方法的 "5.2 times as fast" 利用 char 通常是 ASCII(代码 0 - 127)本身值得注意。
  • 作为一般原则,出现在标准库实现中的代码在标准 C 环境中不存在。有时这是为了代码的“好处”——它可以依赖标准不提供的特定于实现的保证,就像在这种情况下一样。有时这是对代码的“损害”——例如,可能有一个明显的 X 以 Y 的实现,你不能这样做,因为你已经选择了 Y 的明显的 X 实现。

标签: c undefined-behavior


【解决方案1】:

尽管这在技术上是未定义的行为,但实际上,没有原生架构以比单词大小更精细的粒度检查越界内存访问。因此,虽然通过终结符的垃圾最终可能会被读取,但结果不会是崩溃。

【讨论】:

  • @Jongware 实际上,例如,读取未初始化的变量本身就是未定义的行为,因此越界访问数组也是如此。
  • @Jongware 对您来说“听起来不错”的内容不必同意标准中的内容。标准说它是 UB,所以它是 UB,句号。
  • @Jongware strlen()。我不怀疑它适用于实际的特定实现,尽管是 UB,但这只是因为标准库的编写者对平台和/或他们所使用的编译器有深入的了解使用。从某种意义上说,库/其他实现代码依赖于 UB 的某些方面是可以的,但用户代码这样做是不行的。
  • @Jongware:该实现有额外的保证,即标准没有强制要求,并在 strlen() 的 内部 实现中滥用它们。你不关心 strlen() 是如何在内部实现的,你使用标准承诺你 abot strlen() 的保证。
  • @Jongware:它 UB:你不能获取该代码并将其移动到任意架构,最值得注意的是!MMU(非-paged based) 架构,其中字符串可能在数据段的末尾结束,该数据段可能在 MPU 处设置了字节粒度限制。
【解决方案2】:

如果数组不够长并且我们正在读取它的结尾,我根本不明白为什么对齐会有任何相关性。

例程从对齐字边界开始,原因有两个:首先,在大多数架构上,从对齐地址读取字会更快(在少数 CPU 上也是强制)。速度提升足以在许多类似操作中使用相同的技巧:memcpy、strcpy、memmove、memchr 等。

第二:如果您继续阅读单词从单词边界开始,您可以确保字符串的其余部分驻留在同一个内存页中。一个字符串(包括它的终止零)不能跨越内存页面边界,也不能读取一个单词。 (1)

所以这总是最快和最安全的,即使内存页面粒度是 sizeof(LONG_BIT) (它不是)。

在字符串末尾附近拾取整个单词可能会在最后一个零之后拾取额外的字节,但从有效内存中读取未定义字节不是 UB - 仅对其内容起作用是 (2)。如果单词内部的任何地方都包含零终止符,则使用test_byte 检查各个字节,并且如原始源中所示,这永远不会作用终止符之后的字节。

(1) 显然他们可以,但我的意思是“永远不会进入锁定页面”或类似的东西。

(2) 正在讨论中。请参阅 Sneftel 的回答(对此感到抱歉!)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-07-13
    • 2011-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-17
    • 2015-06-15
    • 1970-01-01
    相关资源
    最近更新 更多