我正在浏览strlen
代码here,我想知道代码中使用的优化是否真的需要?例如,为什么像下面这样的东西不能同样好或更好?
unsigned long strlen(char s[]) {
unsigned long i;
for (i = 0; s[i] != '\0'; i++)
continue;
return i;
}
对于编译器来说,更简单的代码不是更好和/或更容易优化吗?
在链接后面的页面上的strlen
的代码看起来像这样:
/* Copyright (C) 1991, 1993, 1997, 2000, 2003 Free Software Foundation, Inc.
This file is part of the GNU C Library.
Written by Torbjorn Granlund ([email protected]),
with help from Dan Sahlin ([email protected]);
commentary by Jim Blandy ([email protected]).
The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA. */
#include <string.h>
#include <stdlib.h>
#undef strlen
/* Return the length of the null-terminated string STR. Scan for
the null terminator quickly by testing four bytes at a time. */
size_t
strlen (str)
const char *str;
{
const char *char_ptr;
const unsigned long int *longword_ptr;
unsigned long int longword, magic_bits, himagic, lomagic;
/* Handle the first few characters by reading one character at a time.
Do this until CHAR_PTR is aligned on a longword boundary. */
for (char_ptr = str; ((unsigned long int) char_ptr
& (sizeof (longword) - 1)) != 0;
++char_ptr)
if (*char_ptr == '\0')
return char_ptr - str;
/* All these elucidatory comments refer to 4-byte longwords,
but the theory applies equally well to 8-byte longwords. */
longword_ptr = (unsigned long int *) char_ptr;
/* Bits 31, 24, 16, and 8 of this number are zero. Call these bits
the "holes." Note that there is a hole just to the left of
each byte, with an extra at the end:
bits: 01111110 11111110 11111110 11111111
bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
The 1-bits make sure that carries propagate to the next 0-bit.
The 0-bits provide holes for carries to fall into. */
magic_bits = 0x7efefeffL;
himagic = 0x80808080L;
lomagic = 0x01010101L;
if (sizeof (longword) > 4)
{
/* 64-bit version of the magic. */
/* Do the shift in two steps to avoid a warning if long has 32 bits. */
magic_bits = ((0x7efefefeL << 16) << 16) | 0xfefefeffL;
himagic = ((himagic << 16) << 16) | himagic;
lomagic = ((lomagic << 16) << 16) | lomagic;
}
if (sizeof (longword) > 8)
abort ();
/* Instead of the traditional loop which tests each character,
we will test a longword at a time. The tricky part is testing
if *any of the four* bytes in the longword in question are zero. */
for (;;)
{
/* We tentatively exit the loop if adding MAGIC_BITS to
LONGWORD fails to change any of the hole bits of LONGWORD.
1) Is this safe? Will it catch all the zero bytes?
Suppose there is a byte with all zeros. Any carry bits
propagating from its left will fall into the hole at its
least significant bit and stop. Since there will be no
carry from its most significant bit, the LSB of the
byte to the left will be unchanged, and the zero will be
detected.
2) Is this worthwhile? Will it ignore everything except
zero bytes? Suppose every byte of LONGWORD has a bit set
somewhere. There will be a carry into bit 8. If bit 8
is set, this will carry into bit 16. If bit 8 is clear,
one of bits 9-15 must be set, so there will be a carry
into bit 16. Similarly, there will be a carry into bit
24. If one of bits 24-30 is set, there will be a carry
into bit 31, so all of the hole bits will be changed.
The one misfire occurs when bits 24-30 are clear and bit
31 is set; in this case, the hole at bit 31 is not
changed. If we had access to the processor carry flag,
we could close this loophole by putting the fourth hole
at bit 32!
So it ignores everything except 128's, when they're aligned
properly. */
longword = *longword_ptr++;
if (
#if 0
/* Add MAGIC_BITS to LONGWORD. */
(((longword + magic_bits)
/* Set those bits that were unchanged by the addition. */
^ ~longword)
/* Look at only the hole bits. If any of the hole bits
are unchanged, most likely one of the bytes was a
zero. */
& ~magic_bits)
#else
((longword - lomagic) & himagic)
#endif
!= 0)
{
/* Which of the bytes was the zero? If none of them were, it was
a misfire; continue the search. */
const char *cp = (const char *) (longword_ptr - 1);
if (cp[0] == 0)
return cp - str;
if (cp[1] == 0)
return cp - str + 1;
if (cp[2] == 0)
return cp - str + 2;
if (cp[3] == 0)
return cp - str + 3;
if (sizeof (longword) > 4)
{
if (cp[4] == 0)
return cp - str + 4;
if (cp[5] == 0)
return cp - str + 5;
if (cp[6] == 0)
return cp - str + 6;
if (cp[7] == 0)
return cp - str + 7;
}
}
}
}
libc_hidden_builtin_def (strlen)
为什么这个版本跑得快?
它不是做了很多不必要的工作吗?
9条答案
按热度按时间jhkqcmku1#
你不需要,你不应该写这样的代码-特别是如果你不是C编译器/标准库供应商。它是用于实现
strlen
的代码,带有一些非常可疑的速度黑客和假设(没有用Assert测试或在评论中提到):unsigned long
为4或8字节unsigned long long
而不是uintptr_t
unsigned long
s更重要的是,一个好的编译器甚至可以取代编写为
(注意,它必须是与
size_t
兼容的类型),编译器内置strlen
的内联版本,或者对代码进行向量化;但是编译器将不太可能能够优化复杂版本。C11 7.24.6.3将
strlen
函数描述为:说明
strlen
函数计算s指向的字符串的长度。返回
strlen
函数返回终止空字符之前的字符数。现在,如果
s
指向的字符串位于一个字符数组中,该字符数组的长度刚好足以包含该字符串和终止NUL,那么如果我们访问经过null终止符的字符串,则行为将是undefined,例如,在因此,在完全可移植/标准兼容的C中,真正正确实现这个的 * 唯一 * 方法是它在你的问题中编写的方式,除了琐碎的转换-你可以通过展开循环等假装更快,但它仍然需要一次 * 一个字节 * 完成。
(As评论者已经指出,当严格的可移植性是一个太大的负担时,利用合理的或已知安全的假设并不总是一件坏事。特别是在代码中,这是一个特定的C实现的一部分。但你必须先了解规则,然后才能知道如何/何时可以弯曲它们。)
链接的
strlen
实现首先逐个检查字节,直到指针指向unsigned long
的自然4或8字节对齐边界。C标准说,访问一个没有正确对齐的指针有undefined behavior**,所以这绝对是为了下一个更脏的伎俩。(实际上,在x86以外的某些CPU架构上,未对齐的字或双字加载将出错。C不是一种可移植的汇编语言,但这段代码就是这样使用的)。这也使得读取对象的末尾成为可能,而不会有在对齐块中进行内存保护的实现出错的风险(例如,4kiB虚拟内存页)。接下来是肮脏的部分:代码 * 打破 * 承诺,一次读取4或8个8位字节(a
long int
),并使用带无符号加法的位技巧来快速找出这4或8个字节中是否有 * 任何 * 零字节-它使用一个特制的数字,这将导致进位位更改位掩码捕获的位。本质上,这将找出掩码中的4或8个字节中是否有任何一个是零,这比循环遍历这些字节中的每一个要快。最后,在结尾处有一个循环来计算 * 哪个 * 字节是第一个零,如果有的话,并返回结果。最大的问题是,在
sizeof (unsigned long)
情况下,sizeof (unsigned long) - 1
会读取字符串的末尾-只有当空字节在 * 最后 * 访问的字节中时(即,在little-endian中是最重要的,在big-endian中是最不重要的),它是否 * 不 * 访问越界的数组!即使在C标准库中用于实现
strlen
的代码也是 * 坏 * 代码。它有几个实现定义和未定义的方面,它不应该在任何地方使用,而不是系统提供的strlen
-我在这里将函数重命名为the_strlen
,并添加了以下main
:缓冲区的大小经过仔细调整,以便它可以准确地保存
hello world
字符串和终止符。但是在我的64位处理器上,unsigned long
是8字节,所以对后一部分的访问将超过这个缓冲区。如果我现在用
-fsanitize=undefined
和-fsanitize=address
编译并运行结果程序,我得到:也就是说,发生了不好的事情。
bejyjqdl2#
关于这方面的一些细节/背景,评论中有很多(轻微或完全)错误的猜测。
您正在查看glibc的优化C回退优化实现。(对于没有手写asm实现的ISA)。或者是该代码的旧版本,它仍然在glibc源代码树中。https://code.woboq.org/userspace/glibc/string/strlen.c.html是一个基于glibc git树的代码浏览器。显然,它仍然被一些主流的glibc目标使用,包括MIPS。(谢谢@zwol)。
在x86和ARM等流行ISA上,glibc使用手写asm
因此,对这段代码进行任何修改的动机比您想象的要低。
这个bithack代码(https://graphics.stanford.edu/zeroseander/bithacks.html#ZeroInWord)并不是在你的服务器/台式机/笔记本电脑/智能手机上实际运行的代码。它比一次一个字节的简单循环要好,但是即使是这种bithack与现代CPU的高效asm相比也是相当糟糕的(特别是x86,其中AVX 2 SIMD允许用几条指令检查32个字节,如果数据在现代CPU的L1 d缓存中是热的,则主循环中每个时钟周期允许32到64个字节,具有2/时钟向量负载和ALU吞吐量。即,对于启动开销不占主导地位的中等大小的串。
glibc使用动态链接技巧将
strlen
解析为适合您的CPU的最佳版本,因此即使在x86中也存在SSE2 version(16字节向量,x86-64的基线)和AVX2 version(32字节向量)。x86在向量寄存器和通用寄存器之间具有高效的数据传输,这使得它具有独特的(?)有利于使用SIMD来加速隐式长度字符串上的函数,其中循环控制依赖于数据。
pcmpeqb
/pmovmskb
可以一次测试16个单独的字节。glibc有一个类似using AdvSIMD的AArch 64版本,还有一个用于AArch 64 CPU的版本,其中vector->GP寄存器会暂停管道,因此它会执行actually use this bithack。但是一旦命中,就使用计数前导零来查找寄存器内的字节,并在检查跨页后利用AArch 64的高效非对齐访问。
还涉及:为什么在启用GCC优化的情况下,使用strlen的代码要慢6.5倍?有更多关于什么是快速与在x86 asm for
strlen
中速度很慢,因为它有一个大缓冲区和一个简单的asm实现,这可能有助于gcc了解如何内联。(一些gcc版本不明智地内联rep scasb
,这是非常慢的,或者像这样一次4字节的bithack。因此GCC的inline-strlen recipe需要更新或禁用。Asm没有C风格的“未定义行为”;无论你喜欢怎样访问内存中的字节都是安全的,并且包括任何有效字节的对齐加载不会出错。内存保护以对齐页粒度进行;对齐的访问比不能跨越页面边界窄。Is it safe to read past the end of a buffer within the same page on x86 and x64?同样的推理也适用于这个C黑客让编译器为这个函数的独立非内联实现创建的机器代码。
当编译器发出代码来调用未知的非内联函数时,它必须假设该函数修改了任何/所有全局变量和它可能有指针的任何内存。也就是说,除了没有进行地址转义的局部变量之外,所有的变量都必须在调用过程中在内存中同步。显然,这适用于用asm编写的函数,也适用于库函数。如果您不启用链接时优化,它甚至会应用于单独的翻译单元(源文件)。
为什么这是安全的 * 作为glibc的一部分 * 但 * 不 * 否则。
最重要的因素是,这个
strlen
不能内联到任何其他东西。这是不安全的;它包含严格混叠UB(通过unsigned long*
阅读char
数据)。char*
被允许别名任何其他but the reverse is not true。这是一个提前编译库(glibc)的库函数。**它不会与链接时优化内联到调用者中。**这意味着它只需要编译为
strlen
独立版本的安全机器码。它不必是便携式/安全的C。GNU C库只需要使用GCC编译。显然不支持用clang或ICC编译它,尽管它们支持GNU扩展。GCC是一个提前编译器,它将C源文件转换为机器代码的目标文件。不是解释器,所以除非它在编译时内联,否则内存中的字节只是内存中的字节。也就是说,当不同类型的访问发生在不同的函数中时,严格别名UB并不危险,这些函数之间没有内联。
请记住,
strlen
的行为是由ISO C标准定义的。这个函数名是实现的一部分。像GCC这样的编译器甚至将名称视为内置函数,除非您使用-fno-builtin-strlen
,因此strlen("foo")
可以是编译时常量3
。库中的定义 * 仅 * 在gcc决定实际发出对它的调用而不是内联自己的配方或其他东西时使用。当UB在编译时对编译器不可见时,你得到的是正常的机器码。机器代码必须为无UB的情况工作,即使你想,asm也没有办法检测调用者用来将数据放入指向内存的类型。
Glibc被编译成一个独立的静态或动态库,不能与链接时优化内联。glibc的构建脚本不会创建包含机器码+ gcc GIMPLE内部表示的“胖”静态库,以便在内联到程序中时进行链接时优化。(即
libc.a
不会参与-flto
链接时优化到主程序中。)以这种方式构建glibc对于实际使用此.c
* 的目标可能不安全 *。事实上,正如@zwol评论的那样,在构建glibc * 本身 * 时不能使用LTO,因为如果glibc源文件之间可以内联,像这样的“脆弱”代码可能会中断。(
strlen
有一些内部用途,例如:可能作为printf
实现的一部分)此
strlen
做了一些假设:***
CHAR_BIT
是8的倍数。在所有GNU系统上都是真的。POSIX 2001甚至保证了CHAR_BIT == 8
。(对于CHAR_BIT= 16
或32
的系统,这看起来是安全的,比如一些DSP; unaligned-prologue循环将总是运行0次迭代,如果sizeof(long) = sizeof(char) = 1
,因为每个指针总是对齐的,p & sizeof(long)-1
总是零。)但如果你有一个非ASCII字符集,其中字符是9或12位宽,0x8080...
是错误的模式。unsigned long
可能是4或8字节。或者,它实际上可以适用于unsigned long
的任何大小,最大为8,它使用assert()
来检查。这两个是不可能的UB,他们只是不可移植到一些C实现。这段代码是(或曾经是)C实现的一部分,在它工作的平台上,这很好。
下一个假设是潜在的C UB:
*包含任何有效字节的对齐加载不会出错,只要忽略实际需要的对象之外的字节,它就是安全的。(在每个GNU系统上的asm中是真的,在所有普通的CPU上也是真的,因为内存保护是以对齐页面的粒度进行的。Is it safe to read past the end of a buffer within the same page on x86 and x64?在C中是安全的,当UB在编译时不可见时。如果没有内联,这里就是这种情况。编译器不能证明阅读超过第一个
0
是UB;例如,它可以是一个包含{1,2,0,3}
的Cchar[]
数组)最后一点是什么使它安全地读取过去的C对象的结束在这里。即使在与当前编译器内联时,这也是非常安全的,因为我认为他们目前不认为执行路径是不可访问的。但无论如何,如果你让它内联的话,严格的别名已经是一个障碍了。
然后,您会遇到一些问题,比如Linux内核的旧的不安全
memcpy
*CPP宏 *,它使用指向unsigned long
的指针转换(gcc、严格别名和恐怖故事)。(现代Linux使用-fno-strict-aliasing
编译,而不是小心使用may_alias
属性。)这个
strlen
可以追溯到你可以摆脱这样的东西的时代;在GCC 3之前,它曾经非常安全,即使没有“只有在不内联的时候”的警告。只有在跨越调用/重试边界时才可见的UB不会伤害我们。(例如,在
char buf[]
上调用此函数,而不是在unsigned long[]
转换为const char*
的数组上调用)。一旦机器码被固定下来,它就只处理内存中的字节了。非内联函数调用必须假设被调用方读取任何/所有内存。安全写入,不使用严格别名UB
GCC type属性
may_alias
为类型提供了与char*
相同的alias-anything处理。(由@KonradBorowsk建议)。GCC标头当前将其用于x86 SIMD向量类型,如__m128i
,因此您可以始终安全地执行_mm_loadu_si128( (__m128i*)foo )
。(参见Isreinterpret_cast
ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?了解更多关于这意味着什么和不意味着什么的细节。您可以使用
aligned(1)
来表示alignof(T) = 1
类型。typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
。这对于strlen
的未对齐启动部分可能很有用,如果你不只是在第一个对齐边界之前一次执行字符。(主循环需要对齐,这样如果终止符正好在未Map的页面之前,您就不会出错。在ISO中表达别名加载的一种可移植方式是使用
memcpy
,现代编译器确实知道如何将其作为单个加载指令内联。例如这也适用于未对齐的负载,因为
memcpy
通过char
-at-a-time访问来工作。但实际上,现代编译器对memcpy
的理解非常好。这里的危险是,如果GCC不确定
char_ptr
是字对齐的,它就不会在一些不支持asm中的非对齐加载的平台上内联它。例如,MIPS 64 r6之前的MIPS或更早的ARM。如果你对memcpy
进行了一个实际的函数调用,只是为了加载一个单词(并将其留在其他内存中),那将是一场灾难。GCC有时可以看到代码何时对齐指针。或者在到达ulong边界的一次一个字符循环之后,您可以使用p = __builtin_assume_aligned(p, sizeof(unsigned long));
这并不能避免读过对象的可能的UB,但对于当前的GCC,这在实践中并不危险。
为什么需要手工优化的C源代码:当前的编译器还不够好,
当你想让一个广泛使用的标准库函数的性能下降到最后一点时,手工优化的asm可能会更好。特别是对于像
memcpy
这样的东西,还有strlen
。在这种情况下,使用带有x86内部函数的C来利用SSE 2并不容易。但在这里,我们只是在谈论一个天真与。bithack C版本,没有任何ISA特定的功能。
(我认为我们可以假定
strlen
的使用已经足够广泛,因此让它尽可能快地运行是很重要的。因此,问题变成了我们是否可以从更简单的源代码中获得有效的机器代码。不,我们不能。)当前的GCC和clang无法自动向量化循环,其中迭代计数在第一次迭代之前未知。(例如,在运行第一次迭代之前,必须能够检查循环是否将运行至少16次迭代。自动向量化memcpy是可能的(显式长度缓冲区),但不是strcpy或strlen(隐式长度字符串),给定当前编译器。
这包括搜索循环,或任何其他具有依赖于数据的
if()break
和计数器的循环。ICC(Intel的x86编译器)可以自动向量化一些搜索循环,但仍然只能像OpenBSD的libc使用的那样,为简单/幼稚的C
strlen
一次一字节地进行asm。(Godbolt)。(来自@Peske的回答)手工优化的libc
strlen
对于当前编译器的性能是必要的。当主内存可以保持每个周期大约8个字节,而L1 d缓存可以在每个周期提供16到64个字节时,每次1个字节(在宽超标量CPU上每个周期展开可能2个字节)是可悲的。(自Haswell和Ryzen以来,在现代主流x86 CPU上,每个周期有2个32字节的负载。不包括AVX 512,它可以降低时钟速度,只是为了使用512位矢量;这就是为什么glibc可能并不急于添加AVX 512版本。虽然使用256位向量,AVX 512 VL + BW掩码比较到掩码中,ktest
或kortest
可以通过减少uops /迭代来使strlen
更加超线程友好。我在这里包括非x86,这是“16字节”。大多数AArch 64 CPU至少可以做到这一点,我认为,有些当然更多。而有些则有足够的
strlen
执行吞吐量来跟上负载带宽。当然,处理大字符串的程序通常应该跟踪长度,以避免经常重新查找隐式长度的C字符串的长度。但是短到中等长度的性能仍然受益于手写实现,我相信有些程序最终会在中等长度的字符串上使用strlen。
ipakzgxi3#
在您链接的文件中的注解中解释了这一点:
以及:
在C中,可以详细地推理效率。
遍历各个字符寻找空值的效率低于一次测试多个字节的效率,就像下面的代码一样。
额外的复杂性来自于需要确保被测字符串在正确的位置对齐,以便一次开始测试多个字节(沿着一个长字边界,如注解中所述),以及需要确保在使用代码时不违反关于数据库大小的假设。
在 * 大多数 *(但不是全部)现代软件开发中,这种对效率细节的关注是不必要的,或者不值得额外的代码复杂性成本。
像这样关注效率确实有意义的一个地方是在标准库中,就像你链接的例子。
如果您想了解更多关于单词边界的信息,请参阅this question和this excellent wikipedia page
我也认为this answer above是一个更清晰和更详细的讨论。
lsmepo6l4#
除了这里的答案,我想指出的是,问题中链接的代码是用于GNU的
strlen
实现的。OpenBSD implementation of
strlen
与问题中提出的代码非常相似。实现的复杂性由作者决定。编辑:我上面链接的OpenBSD代码看起来是没有自己的asm实现的ISA的回退实现。
strlen
有不同的实现,具体取决于体系结构。例如,amd64strlen
的代码是asm。类似于PeterCordes的comments/answer指出非回退GNU实现也是asm。9vw9lbht5#
简而言之,这是一个性能优化,标准库可以通过知道它是用什么编译器编译的-你不应该写这样的代码,除非你写的是一个标准库,可以依赖于一个特定的编译器。具体来说,它同时处理字节的对齐数-在32位平台上为4,在64位平台上为8。这意味着它可以比简单的字节迭代快4或8倍。
要解释这是如何工作的,请考虑下面的图像。这里假设是32位平台(4字节对齐)。
让我们说,字母“H”的“你好,世界!“字符串作为
strlen
的参数提供。由于CPU喜欢在内存中对齐(理想情况下是address % sizeof(size_t) == 0
),因此对齐之前的字节将使用慢速方法逐字节处理。然后,对于每个内存大小的块,通过计算
(longbits - 0x01010101) & 0x80808080 != 0
,它检查整数中是否有任何字节为零。当至少有一个字节高于0x80
时,此计算会出现误报,但通常情况下它应该工作。如果不是这种情况(如黄色区域所示),则长度将增加对齐大小。如果一个整数中的任何一个字节被证明是零(或
0x81
),那么这个字符串将被逐字节检查以确定零的位置。这可能会造成越界访问,但是因为它在对齐范围内,所以很可能是好的,内存Map单元通常没有字节级精度。
s4chpxco6#
您希望代码正确、可维护且快速。这些因素具有不同的重要性:
“正确”是绝对必要的。
“维护”取决于你要维护代码的程度:strlen作为标准C库函数已有40多年的历史。不会变的。因此,可维护性对于该功能来说是相当不重要的。
“快速”:在许多应用程序中,strcpy,strlen等。使用大量的执行时间。通过改进编译器来实现与这个复杂但不太复杂的strlen实现相同的总体速度增益将需要巨大的努力。
快速还有另一个好处:当程序员发现调用“strlen”是他们可以测量字符串中字节数的最快方法时,他们就不会再编写自己的代码来加快速度了。
所以对于strlen来说,速度比你将要编写的大多数代码更重要,而可维护性则不那么重要。
为什么要这么复杂?假设你有一个1,000字节的字符串。简单的实现将检查1,000个字节。当前的实现可能一次检查64位字,这意味着125个64位或8字节字。它甚至可以使用向量指令一次检查32个字节,这将更加复杂,甚至更快。使用向量指令会导致代码稍微复杂一些,但非常简单,检查64位字中的八个字节中是否有一个为零需要一些聪明的技巧。因此,对于中长字符串,这段代码可以预期快四倍。对于像strlen这样重要的函数,值得编写一个更复杂的函数。
PS.代码不是很便携。但它是标准C库的一部分,标准C库是实现的一部分--它不需要是可移植的。
有人贴了一个例子,调试工具抱怨访问字符串末尾之后的字节。可以设计一个实现来保证以下内容:如果p是指向一个字节的有效指针,那么对同一对齐块中的一个字节的任何访问(根据C标准,这将是未定义的行为)都将返回一个未指定的值。
PPPS.英特尔已经在他们后来的处理器中添加了指令,这些指令构成了strstr()函数(在字符串中查找子字符串)的构建块。他们的描述令人难以置信,但他们可以使这个特定的功能快100倍。(基本上,给定一个包含“Hello,world!“和一个以16字节“HelloHelloHelloH”开始并包含更多字节的数组B,它会发现字符串a在B中出现的时间不会早于索引15处开始的时间)。
zzlelutf7#
简而言之:在一次可以获取大量数据的架构上,逐字节检查字符串可能会很慢。
如果空终止的检查可以在32位或64位的基础上完成,它减少了编译器必须执行的检查量。这就是链接代码试图做的,考虑到一个特定的系统。它们对寻址、对齐、缓存使用、非标准编译器设置等做出假设。
在8位CPU上,或者在编写用标准C编写的可移植库时,像您的示例中那样逐字节阅读是一种明智的方法。
查看C标准库以获得如何编写快速/良好代码的建议并不是一个好主意,因为它将是不可移植的,并且依赖于非标准的假设或定义不佳的行为。如果你是一个初学者,阅读这样的代码可能会比教育更有害。
3htmauhk8#
为什么像下面这样的东西不能同样好或更好呢?
OP的代码有功能错误。
但很容易修改。
在编写可移植代码时,需要注意首先使函数正确,然后再考虑性能改进。
即使是非常简单的,看起来正确的代码也可能在功能上有缺陷。
类型
size_t
的范围内,其可以不同于unsigned long
。函数签名与size_t (*f)() = strlen
不匹配。不常见的平台问题,其中ULONG_MAX < SIZE_MAX
和字符串长度是巨大的。const
s
应该是const char *
。非2的补码
(This关注影响到的处理器数量少得可怜,所以实际上只是学究式的关注。非2的补码可能会在下一个C(C23?)).
当
char
是 * 有符号 * 而不是2的补码时,s[i] != '\0'
可能在-0触发。它不应该。str...()
的功能就像字符被访问为unsigned char
一样。对于本小节中的所有函数,每个字符都应该被解释为具有
unsigned char
类型(因此每个可能的对象表示都是有效的,并且具有不同的值)。修复OP的简单代码的这些方面
现在有了一个更好的,可移植的
strlen()
候选者,看看它与“复杂”的替代品的比较。tag5nh1u9#
其他答案没有提到的一个重要的事情是,FSF非常谨慎地确保专有代码不会进入GNU项目。在GNU Coding Standards中,在引用专有程序下,有一个关于以一种不能与现有专有代码混淆的方式组织您的实现的警告:
在任何情况下,都不要在GNU工作中引用Unix源代码!(或任何其他专有程序。)
如果你对一个Unix程序的内部结构有一个模糊的记忆,这并不意味着你绝对不能写一个模仿它,但是试着在内部沿着沿着不同的路线组织模仿,因为这可能会使Unix版本的细节与你的结果无关或不相似。
例如,Unix实用程序通常被优化以最小化内存使用;* 如果你追求速度 *,你的程序将非常不同。
(重点是我的。)