理解 GNU libc 的 strcmp 函数

5

这里是我在glibc中找到的strcmp函数:

int
STRCMP (const char *p1, const char *p2)
{
  const unsigned char *s1 = (const unsigned char *) p1;
  const unsigned char *s2 = (const unsigned char *) p2;
  unsigned char c1, c2;

  do
    {
      c1 = (unsigned char) *s1++;
      c2 = (unsigned char) *s2++;
      if (c1 == '\0')
        return c1 - c2;
    }
  while (c1 == c2);

  return c1 - c2;
}

这是一个相当简单的函数,在while循环体中,用*s1*s2的值初始化c1c2,并继续执行,直到c1nulc1c2的值相等,然后返回c1c2之间的差异。

我不理解的是为什么要使用s1s2变量。除了它们是unsigned char类型外,它们还像参数p1p2一样是const类型。那么为什么不在while循环体内直接使用p1p2并进行强制转换呢?在这种情况下,使用这两个额外的变量是否使函数更加优化?因为这里是我在github上找到的FreeBSD版本的相同函数:

int
strcmp(const char *s1, const char *s2)
{
    while (*s1 == *s2++)
        if (*s1++ == '\0')
            return (0);
    return (*(const unsigned char *)s1 - *(const unsigned char *)(s2 - 1));
}

在他们的版本中,他们甚至没有使用任何额外的变量。
提前感谢您的答复。
PS:在询问这个问题之前,我确实在互联网上搜索了这个特定的事实,但是我什么也没找到。
我还想知道为什么glibc使用这些额外变量而不是直接在while里强制转换参数p1和p2。

1
这个问题会给你答案吗?https://dev59.com/ZnM_5IYBdhLWcg3wfTO7?rq=1 - OznOg
3
我认为治疗glibc并不容易,因为它有很多版本,这取决于架构等因素。只需查看https://sourceware.org/git/?p=glibc.git;a=tree;f=sysdeps/x86_64/multiarch;hb=921595d151ee1661cc5476bb019483e12b7b47f6。 - muradm
我原以为它省略了我想要优化函数的事实,但我后来发现我又谈到了优化,你认为第一部分令人困惑吗?我应该重新做你的更改吗? - localhost
1
如果你的主要问题是如何优化函数,那么你正在查看错误的代码。正如muradm所说,应该查看类似于https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/x86_64/multiarch/strcmp-sse42.S;h=5a0c6668a7e795a39acfd8ef2037d0720271bfff;hb=921595d151ee1661cc5476bb019483e12b7b47f6这样的东西。如果你想知道为什么纯C回退版本写成这样,优化并不是(直接)相关的。 - melpomene
好的,我在C99和C17之间的不同章节编号上感到困惑。 https://dev59.com/ZnM_5IYBdhLWcg3wfTO7?rq=1 真的是一个重复的问题。请其他一些有经验的用户来决定是否关闭该问题,我会退出讨论。 - Lundin
显示剩余3条评论
2个回答

2

您当然是正确的。其中一个转换应该就足够了。特别是如果指针被强制转换,那么检索到的值的转换就是无操作。


这是使用gcc -O3编译的x86-64,用于不必要转换的代码:

STRCMP:
.L4:
        addq    $1, %rdi
        movzbl  -1(%rdi), %eax
        addq    $1, %rsi
        movzbl  -1(%rsi), %edx
        testb   %al, %al
        je      .L7
        cmpb    %dl, %al
        je      .L4
        subl    %edx, %eax
        ret
.L7:
        movzbl  %dl, %eax
        negl    %eax
        ret

这是不需要进行不必要转换的代码:

STRCMP:
.L4:
        addq    $1, %rdi
        movzbl  -1(%rdi), %eax
        addq    $1, %rsi
        movzbl  -1(%rsi), %edx
        testb   %al, %al
        je      .L7
        cmpb    %dl, %al
        je      .L4
        subl    %edx, %eax
        ret
.L7:
        movzbl  %dl, %eax
        negl    %eax
        ret

它们是相同的。


然而,现在大多数情况下仅仅是历史遗留问题。如果char有符号的 并且 有符号表示不是二进制补码,

*(const unsigned char *)p1

并且

(unsigned char)*p1

这两个操作并不相同。前者重新解释位模式,而后者使用模算术转换值。这仅具有历史意义,因为甚至连GCC也不支持没有二进制补码表示的任何架构。而它是最多端口的编译器。


前者显然不符合规范。请参阅C17 7.24.4“比较函数memcmp、strcmp和strncmp返回的非零值的符号由比较对象中第一对不同字符(均解释为无符号字符)的差异的符号确定。”这似乎是他们强制转换为无符号字符的原因,正如您第二个示例所示。 - Lundin
是的,这似乎有点混淆。他们本可以写成 c1 = (unsigned char) *p1++; - Lundin

2
我不理解的是 s1 和 s2 变量的用途。除了它们是无符号字符之外,它们像 p1 和 p2 两个参数一样也是 const 的,那为什么不直接在 while 循环体内使用 p1 和 p2 并将它们进行强制类型转换呢?
出于可读性考虑;使我们人类更容易维护代码。
如果您查看 glibc 源代码,代码倾向于可读性而不是简洁的表达式。这似乎是一个好策略,因为它让它保持了超过30年的活力(积极维护)。
在这种情况下,使用这两个额外变量会使函数更加优化吗?
不,完全没有。
我还想知道 glibc 为什么要使用这些额外变量而不是直接在 while 循环内对参数 p1 和 p2 进行类型转换。
只是为了可读性。
作者知道使用的 C 编译器应该能够对这段代码进行优化。 (而且很容易证明这一点,只需要查看编译器生成的代码即可。对于 GCC,您可以使用 -S 选项,或者使用 binutils 的 objdump -d 来检查一个目标文件或二进制可执行文件。)
请注意,将类型转换为 unsigned char 的原因与 isspace(), isalpha() 等函数相同:比较的字符代码必须被视为 unsigned char 才能得到正确的结果。

2
添加大量的强制类型转换如何提高可读性?相反,它会降低可读性。 - Lundin
1
@Lundin:GNU C库的开发人员认为它应该这样做;这是他们的风格的一部分。他们相当一致地使用了同样的风格。虽然这不是我的首选,我个人认为有更好的风格,但这就是他们的理由。(该项目的长期存在表明他们并不完全错误。)我该如何重新措辞我的答案以使其更清晰? - Nominal Animal
2
@Lundin:你有没有意识到OP并没有问这些转换是否必要,或者为什么要使用它们?你的“主观胡言”完全是你自己的想法,你甚至发明了自己的问题。请重新阅读问题,然后重新评估你的行为。感谢你的投票。 - Nominal Animal
1
OP 询问了这两个额外变量的存在。它们是完全多余的,只有一个目的:使代码更难读。更易读的版本应该写成 c1 = *p1++; - Lundin
@Lundin:我同意这一点。我只声称从glibc开发者的角度来看,它们的预期目的是使代码更易于阅读/维护。实际清理它的补丁可能会在一两年内被接受。 - Nominal Animal
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接