【问题标题】:strlen performance implementationstrlen 性能实现
【发布时间】:2012-08-01 00:50:39
【问题描述】:

这是一个多用途的问题:

  • 这与glibc strlen 实现相比如何?
  • 有没有更好的方法来解决这个问题以及自动矢量化。
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>

/* Todo: Document */
#define WORD_ONES_LOW   ((size_t)-1 / UCHAR_MAX)
#define WORD_ONES_HIGH  (((size_t)-1 / UCHAR_MAX) << (CHAR_BIT - 1))

/*@doc
 * @desc: see if an arch word has a zero
 * #param: w - string aligned to word size
 */
static inline bool word_has_zero(const size_t *w)
{
    return ((*w - WORD_ONES_LOW) & ~*w & WORD_ONES_HIGH);
}

/*@doc
 * @desc: see POSIX strlen()
 * @param: s - string
 */
size_t strlen(const char *s)
{
    const char *z = s;

    /* Align to word size */
    for (; ((uintptr_t)s & (sizeof(size_t) - 1)) && *s != '\0'; s++);

    if (*s != '\0') {
        const size_t *w;

        for (w = (const size_t *)s; !word_has_zero(w); w++);
        for (s = (const char *)w; *s != '\0'; s++);
    }

    return (s - z);
}

【问题讨论】:

标签: c string performance strlen


【解决方案1】:

嗯,这个实现基于与您链接的 glibc 实现几乎相同的技巧 (Determine if a word has a zero byte)。它们做的事情几乎相同,除了在 glibc 版本中,一些循环被展开并且位掩码被明确地拼写出来。您发布的代码中的ONESHIGHS 正是himagic = 0x80808080Llomagic = 0x01010101L 形式的glibc 版本。

我看到的唯一区别是 glibs 版本使用稍微不同的标准来检测零字节

if ((longword - lomagic) & himagic)

不执行... &amp; ~longword(与您示例中的HASZERO(x) 宏相比,它与x 执行相同的操作,但还包括~(x) 成员)。显然 glibc 的作者认为这个更短的公式更有效。然而,它可能导致误报。所以他们检查if下的误报。

这确实是一个有趣的问题,什么更有效:单阶段精确测试(您的代码)或两阶段测试,以粗略的不精确检查开始,然后在必要时进行精确的第二次检查(glibc 代码)。

如果您想了解它们在实际性能方面的比较,请在您的平台和数据上对它们进行计时。没有其他办法。

【讨论】:

  • 请注意,glibc 源文件中的 cmets 似乎是关于 #if 0'd 的代码,它比上述代码更慢且精度略低(只有一种误报情况)。 glibc 源代码中实际使用的代码(在#else 情况下)对于 any 高字节(除了 128)有误报。因此,我的天真结论是 glibc 代码对 ASCII 文本的性能更好但对于任何高字节的东西来说,情况要糟糕得多。
【解决方案2】:

另外,请注意此实现可以在此处读取 char 数组的末尾:

for (w = (const void *)s; !HASZERO(*w); w++);

因此依赖于未定义的行为。

【讨论】:

  • 怎么回事?我们将单词大小与初始 for 循环对齐,然后按单词迭代 '\0' 字符数组必须以空值结尾,否则会发生坏事。我会用不那么钝的代码更新它
  • 示例:一个空的以 null 结尾的字符串是一个大小为 1 的 char 数组。*w 将从内存中读取 4 或 8 个字节,从而访问数组末尾之后的字节。在大多数平台上,这可以正常工作,但它仍然是未定义的行为。
  • 回复:未定义的行为:另一位用户刚刚发布了一个关于此的问题;见stackoverflow.com/q/20312340/978917。共识似乎是这个 is 未定义的行为,但它没关系。对于带有编译器的strlen,它依赖于仅编译器保证的行为。
  • 另一个Q&A about reading past the end of an object in C vs. assembly。它绝对是 C 中的 UB,但是如果您对齐指针,您实际上可以做到这一点,而不会在检查页面粒度访问的实现上出错。 (只要编译器在基于程序不包含 UB 的假设进行优化时没有得出任何不幸的结论...)strlen 用 asm 手写的实现肯定会这样做,但 C 不是 asm。
【解决方案3】:

为了回答你的第二个问题,我认为基于字节的 strlen 实现将导致编译器更好的自动矢量化,如果它很聪明并且支持矢量指令集扩展(例如 SSE)已启用(例如与-msse 或适当的-march)。不幸的是,即使编译器可以生成 32 位或 64 位伪矢量化代码(如问题中引用的 C 代码),它也不会导致任何带有缺少这些功能的基线 CPU 的矢量化,如果它足够聪明......

【讨论】:

  • 编译器仍然不会自动向量化循环进入之前迭代计数未知的循环。因此,不幸的是,搜索循环通常不会自动矢量化。
  • 这真的很不幸。
  • IDK 如果他们选择不这样做,因为 valgrind 之类的东西会抱怨读取超出数组的末尾。不过,这不是唯一的原因,因为在静态数组(或作为 int arr[static 1024] 传递的函数 arg)中搜索时,您仍然无法获得自动向量化。刚刚使用 gcc 和 clang 检查了一个非常简单的 int 数组搜索:godbolt.org/g/7xugwg
  • 哇哦,ICC 会自动矢量化它。 godbolt.org/g/6boZSh。但是 MSVC、gcc 和 clang 在这种微不足道的情况下都失败了。对于非静态数组,ICC 到达对齐边界so reading past the end of an object can't fault。不过,valgrind 还是会不高兴。
猜你喜欢
  • 2016-03-30
  • 2018-11-12
  • 2014-08-05
  • 1970-01-01
  • 2016-04-11
  • 2010-12-16
  • 1970-01-01
  • 2014-04-26
  • 2013-11-30
相关资源
最近更新 更多