危险是多么快?`strlen`?

时间:2015-06-25 23:55:29

标签: c

strlen是一个相当简单的函数,显然是O(n)来计算。但是,我已经看到一些方法一次对多个字符进行操作。请参见示例5 here或此方法here。这些工作的基本方法是将char const*缓冲区重新解释为uint32_t const*缓冲区,然后一次检查四个字节。

就我个人而言,我的直觉反应是,这是一个等待发生的段错误,因为我可能会在有效内存之外取消引用三个字节。然而,这个解决方案似乎很流行,而且我觉得很明显,那些明显破碎的东西经得起时间的考验。

我认为这包括UB有两个原因:

  1. 有效记忆之外的潜在解除引用
  2. 未对齐指针的潜在解除引用
  3. 请注意,没有别名问题;有人可能认为uint32_t别名为不兼容类型,而strlen之后的代码(例如可能代码)更改字符串)可能不按顺序运行到strlen,但事实证明char是严格别名的明确例外 )。

    但是,在实践中失败的可能性有多大?至少,我认为在字符串文字数据部分后需要3字节填充,malloc需要4 - 字节或更大的对齐(实际上大多数系统都是这种情况),{{1需要分配malloc个额外字节。还有与别名相关的其他标准。这对于创建自己的环境的编译器实现来说都很好,但现代硬件上用户代码满足这些条件的频率是多少?

4 个答案:

答案 0 :(得分:3)

该技术有效,如果您调用我们的C库strlen,您将无法避免。例如,如果该库是GNU C库的最新版本(至少在某些目标上),它也会做同样的事情。

使其工作的关键是确保指针正确对齐。如果指针对齐,则操作将足够确定地读取超出字符串末尾的操作,但不会读取到相邻页面中。如果空终止字节位于页面末尾的一个单词内,则将访问该最后一个单词而不触及后续页面。

在C语言中肯定没有明确定义的行为,因此当从一个编译器移植到另一个编译器时,它会带来仔细验证的负担。它还会触发像Valgrind这样的越界访问探测器的误报。

Valgrind不得不修补Glibc这样做。如果没有补丁,就会出现诸如此类的麻烦错误:

==13669== Invalid read of size 8
==13669==    at 0x411D6D7: __wcslen_sse2 (wcslen-sse2.S:59)
==13669==    by 0x806923F: length_str (lib.c:2410)
==13669==    by 0x807E61A: string_out_put_string (stream.c:997)
==13669==    by 0x8075853: obj_pprint (lib.c:7103)
==13669==    by 0x8084318: vformat (stream.c:2033)
==13669==    by 0x8081599: format (stream.c:2100)
==13669==    by 0x408F4D2: (below main) (libc-start.c:226)
==13669==  Address 0x43bcaf8 is 56 bytes inside a block of size 60 alloc'd
==13669==    at 0x402BE68: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==13669==    by 0x8063C4F: chk_malloc (lib.c:1763)
==13669==    by 0x806CD79: sub_str (lib.c:2653)
==13669==    by 0x804A7E2: sysroot_helper (txr.c:233)
==13669==    by 0x408F4D2: (below main) (libc-start.c:226)

Glibc正在使用SSE指令一次计算wcslen八个字节(而不是四个,wchar_t的宽度)。在这样做时,它在60字节宽的块中的偏移56处进行访问。但请注意,此访问权限永远不会跨越页面边界:地址可以被8整除。

如果您使用汇编语言工作,则不必再考虑该技术。

事实上,在我使用的一些优化音频编解码器(目标ARM)中使用了该技术,该编解码器在Neon指令集中具有大量手写汇编语言。

我在使用集成这些编解码器的代码运行Valgrind时注意到了它,并联系了供应商。他们解释说这只是一种无害的循环优化技术;我通过汇编语言说服自己是对的。

答案 1 :(得分:2)

(1)肯定会发生。没有什么可以阻止你在分配页面的末尾附近使用strlen字符串,这可能导致访问超过已分配内存的末尾和一个很大的崩溃。如您所知,这可以通过填充所有分配来缓解,但是您必须让任何库执行相同的操作。更糟糕的是,你必须安排链接器和操作系统总是添加这个填充(记住操作系统在一个静态内存缓冲区中传递argv [])。这样做的开销并不值得。

(2)也肯定会发生。早期版本的ARM处理器会在未对齐访问时生成数据中止,这会导致程序因总线错误而死(或者如果您正在运行裸机,则暂停CPU),或强制非常昂贵的陷阱通过内核来处理未对齐的访问。这些早期的ARM芯片仍然在旧手机和嵌入式设备中广泛使用。后来的ARM处理器合成多个字访问来处理未对齐的访问,但这会导致整体性能降低,因为您基本上需要将内存负载的数量增加一倍。

许多当前("现代")PIC和嵌入式微处理器缺乏处理未对齐访问的逻辑,并且在给定未对齐地址时可能表现不可预测甚至无意义(我个人看到芯片只会掩盖底部的位,这会给出错误的答案,而其他的只会给出带有未对齐访问的垃圾结果)。

因此,在任何应该远程移植的东西中使用这都是非常危险的。请不要使用此代码;使用libc strlen。它通常会更快(适合您的平台优化),并使您的代码可移植。你想要的最后一件事是你的代码在某些情况下(在分配结束附近的字符串)或某些新处理器上巧妙地和意外地中断。

答案 2 :(得分:1)

唐纳德·克努特(Donald Knuth)是一位写过3 +卷聪明算法的人说:"过早优化是所有邪恶的根源"。

strlen()使用了很多,所以它确实应该很快。关于wildplasser的评论,"我相信库函数",是什么让你认为库函数一次工作?还是慢?

标题可能会让人觉得你建议的代码比标准系统库 strlen()更快,但我认为你的意思是它比天真的更快无论如何,strlen()可能都没有被使用。

我编译了一个简单的C程序,并查看了使用GNU的glibc函数的64位系统。我看到的代码非常复杂,在使用寄存器宽度而不是一次使用字节方面看起来非常快。我在 strlen()中看到的代码是用汇编语言编写的,因此如果这是编译的C代码,可能没有垃圾指令。我看到的是rtld-strlen.S。此代码还会展开循环以减少循环开销。

在您认为可以在strlen上做得更好之前,您应该查看该代码,或者特定体系结构的相应代码以及寄存器大小。

如果您确实编写了自己的strlen,请根据现有实现进行基准测试。

显然,如果你使用系统strlen,那么它可能是正确的,你不必担心代码中的优化导致无效的内存引用。

答案 3 :(得分:0)

我同意这是一种bletcherous技术,但我怀疑它可能在大多数时间都有效。如果字符串正好碰到数据(或堆栈)段的末尾,那只是一个段错误。绝大多数字符串(静态或动态分配)都不会。

但是你是对的,保证它正常工作你需要保证所有字符串都以某种方式填充,并且你的垫片列表看起来是正确的。

如果对齐是一个问题,你可以在快速strlen实现中处理它;你不必试图对齐所有字符串。

(但是,当然,如果你的问题是你花了太多时间扫描字符串,那么正确的解决方法不是拼命地尝试使字符串扫描速度更快,而是要进行操作以便你不必首先扫描这么多字符串...)