strlen是一个相当简单的函数,显然是O(n)来计算.但是,我已经看到一些方法一次对多个角色进行操作.见例如5 此处或这种方法在这里.这些工作的基本方法是将char const*缓冲区重新解释为uint32_t const*缓冲区,然后一次检查四个字节.
就个人而言,我的直觉反应是这是一个段错等待发生,因为我可能会在有效内存之外取消引用三个字节.然而,这个解决方案似乎已经存在,似乎很奇怪,明显破碎的东西经得起时间的考验.
我认为这包括UB有两个原因:
(请注意,没有别名问题;有人可能会认为uint32_t别名是不兼容的类型,并且代码strlen(例如可能更改字符串的代码)之后的代码可能会无序运行strlen,但事实证明这char是一个严格别名的显式异常).
但是,在实践中失败的可能性有多大?至少,我认为3在字符串文字数据部分之后需要字节填充,malloc需要是4字节或更大的对齐(实际上大多数系统都是这种情况),并且malloc需要分配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)
Run Code Online (Sandbox Code Playgroud)
Glibc 使用 SSE 指令一次计算wcslen8 个字节(而不是 4 个字节的宽度wchar_t)。在此过程中,它在 60 字节宽的块中的偏移量 56 处进行访问。但请注意,此访问永远不能跨越页边界:地址可被 8 整除。
如果您使用汇编语言,则无需对技术进行三思而后行。
事实上,该技术在我使用的一些优化音频编解码器(针对 ARM)中被大量使用,这些编解码器在 Neon 指令集中采用了大量手写汇编语言。
我在集成这些编解码器的代码上运行 Valgrind 时注意到了这一点,并联系了供应商。他们解释说这只是一种无害的循环优化技术;我仔细研究了汇编语言,并说服自己他们是对的。