为什么glibc中strcpy的性能较差?

3
我正在阅读的源代码。在阅读函数的源代码时,它的性能并不如我所期望的那样好。
以下是中的源代码:
   char * strcpy (char *dest, const char* src)
    {
        reg_char c;
        char *__unbounded s = (char *__unbounded) CHECK_BOUNDS_LOW (src);
        const ptrdiff_t off = CHECK_BOUNDS_LOW (dest) - s - 1;
        size_t n;

        do {
            c = *s++;
            s[off] = c;
        }
        while (c != '\0');

        n = s - src;
        (void) CHECK_BOUNDS_HIGH (src + n);
        (void) CHECK_BOUNDS_HIGH (dest + n);

        return dest;
    }

因为我不知道使用偏移量的原因,所以我通过比较上述代码和以下代码进行了一些性能测试:

char* my_strcpy(char *dest, const char *src)
{
    char *d = dest;
    register char c;

    do {
        c = *src++;
        *d++ = c;
    } while ('\0' != c);

    return dest;
}

作为结果,我的测试表明strcpy的性能较差。我已经删除了有关绑定指针的代码。
为什么glibc版本要使用偏移量?
下面是有关测试的介绍:
1. 平台:x86(Intel(R) Pentium(R) 4),gcc 版本 4.4.2
2. 编译标志:没有标志,因为我不想进行任何优化;命令为gcc test.c
我使用的测试代码如下:
#include <stdio.h>
#include <stdlib.h>

char* my_strcpy1(char *dest, const char *src)
{
    char *d = dest;
    register char c;

    do {
        c = *src++;
        *d++ = c;
    } while ('\0' != c);

    return dest;
}

/* Copy SRC to DEST. */
char *
my_strcpy2 (dest, src)
     char *dest;
     const char *src;
{
  register char c;
  char * s = (char *)src;
  const int off = dest - s - 1;

  do
    {
      c = *s++;
      s[off] = c;
    }
  while (c != '\0');

  return dest;
}

int main()
{
    const char str1[] = "test1";
    const char str2[] = "test2";
    char buf[100];

    int i;
    for (i = 0; i < 10000000; ++i) {
        my_strcpy1(buf, str1);
        my_strcpy1(buf, str2);
    }

    return 0;
}

当使用my_strcpy1函数时,输出结果为:
[root@Lnx99 test]#time ./a.out

real    0m0.519s
user    0m0.517s
sys     0m0.001s
[root@Lnx99 test]#time ./a.out

real    0m0.520s
user    0m0.520s
sys     0m0.001s
[root@Lnx99 test]#time ./a.out

real    0m0.519s
user    0m0.516s
sys     0m0.002s

使用my_strcpy2时,输出结果为:
[root@Lnx99 test]#time ./a.out

real    0m0.647s
user    0m0.647s
sys     0m0.000s
[root@Lnx99 test]#time ./a.out

real    0m0.642s
user    0m0.638s
sys     0m0.001s
[root@Lnx99 test]#time ./a.out

real    0m0.639s
user    0m0.638s
sys     0m0.002s

我知道使用命令time并不是很准确。但是我可以从用户提供的时间得出答案。

更新:

To remove the cost used to calculate the offset, I removed some code and added a global variable.

#include <stdio.h>
#include <stdlib.h>

char* my_strcpy1(char *dest, const char *src)
{
    char *d = dest;
    register char c;

    do {
        c = *src++;
        *d++ = c;
    } while ('\0' != c);

    return dest;
}


int off;

/* Copy SRC to DEST. */
char *
my_strcpy2 (dest, src)
     char *dest;
     const char *src;
{
  register char c;
  char * s = (char *)src;

  do
    {
      c = *s++;
      s[off] = c;
    }
  while (c != '\0');

  return dest;
}

int main()
{
    const char str1[] = "test1test1test1test1test1test1test1test1";
    char buf[100];

    off = buf-str1-1;

    int i;
    for (i = 0; i < 10000000; ++i) {
        my_strcpy2(buf, str1);
    }

    return 0;
}

但是my_strcpy2的性能仍然不如my_strcpy1。然后我检查了汇编代码,但也没有得到答案。

我还扩大了字符串的大小,my_strcpy1的性能仍然优于my_strcpy2


1
能否发布一下您的平台、编译器版本、优化标志以及两个函数的实际时间?谢谢。 - NPE
2
这是 strcpy 的 C 版本,你的平台几乎肯定有一个汇编版本,glibc 会使用它。 - Dietrich Epp
“...我进行了一些性能测试...” 你做了哪些测试?你确定你测试的是优化后的发布版本代码吗?所有这些CHECK_BOUNDS_HIGH宏看起来像是针对调试版本代码(启用“有界指针”支持时)的额外安全检查。在启用这些调试宏的情况下测试性能毫无意义。 - AnT stands with Russia
谢谢您的评论。我已经编辑了我的帖子,并添加了更多信息。 - linuxer
3
哪个未优化版本更快其实并不重要。如果你想要快速的代码,你应该开启编译器优化。所以实际使用中只有在开启编译器优化时,哪个版本更快才是重要的。 - sth
显示剩余2条评论
3个回答

8
它使用了偏移量方法,因为这样可以消除循环中的一个增量 - glibc代码只需要增加,而您的代码需要增加和。 请注意,您正在查看的代码是与体系结构无关的后备实现 - glibc具有许多体系结构的覆盖汇编实现(例如x86-64 strcpy())。

2
更好的是,现代编译器将这种类型的函数视为内置函数,因此备用代码几乎永远不会被使用。 - Jens Gustedt
1
我理解你的意思,但我想学习那些与架构无关的代码。 - linuxer
昨天我考虑了计算偏移量所用的成本。我移动了用于计算偏移量的代码,并添加了一个新的全局变量。但结果并没有改变。我已经在帖子中添加了最新的测试代码。 - linuxer
2
@user411318:与架构无关的代码在任何特定架构上都不一定是最快的 - 尤其是在像 Pentium 4 这样的架构奇怪的设计上。这就是为什么有架构特定的覆盖!在这里进行的权衡 - 变量指针偏移量中少了一个增量 - 在不同的 CPU 上会有不同的效果。您需要进行更广泛的测试(包括更长的字符串)来完全评估设计。 - caf
我明白你的意思。也许这就是原因。glibc中strcpy的通用代码对x86不太友好。 - linuxer

1

1

根据我所看到的,你的代码更快并不让我感到惊讶。

看看循环,你的循环和glibc的循环几乎完全相同。但是glibc在前后都有额外的代码...

一般来说,简单的偏移量不会降低性能,因为x86允许相当复杂的间接寻址方案。因此,这两个循环可能以相同的速度运行。

编辑:这是我根据你提供的额外信息更新的内容。

你的字符串大小只有5个字符。尽管偏移量方法“可能”在长期内略微更快,但需要在开始循环之前计算偏移量的多个操作使其在短字符串上变慢。也许如果你尝试更大的字符串,差距将会缩小,甚至可能完全消失。


谢谢你的更新。昨天我也考虑了用于计算偏移量的成本。然后我删除了偏移量的计算。添加一个全局变量offset,在主函数中循环外计算。但是my_strcpy2的性能仍然比my_strcpy1差。 - linuxer
那么此时,最有可能是由于CPU流水线的原因。也许你的旧Pentium 4无法有效地处理间接寻址。我直到晚期Core 2时代才开始涉足HPC领域,所以我对Pentium一无所知。(在Pentium时代我还在上高中...) - Mysticial
我反汇编了my_strcpy1和my_strcpy2,它们的循环汇编代码非常相似。我没有发现任何有价值的信息。 - linuxer
你已经尽可能地完成了。唯一剩下的就是获得一个周期精确的 Pentium 4 模拟器并观察流水线。虽然我敢打赌,除了英特尔内部,它们不存在。你最好在其他机器上运行此代码以查看它们的行为。 - Mysticial
我在另一台CPU为“Pentium(R) Dual-Core CPU E5300”的机器上测试了代码。然而,my_strcpy2的性能仍然较差。 - linuxer

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