【问题标题】:Why are multiple if statements faster than executing a while loop?为什么多个 if 语句比执行 while 循环更快?
【发布时间】:2019-03-22 07:56:27
【问题描述】:

我的程序的输入是一个大字符串,大约 30,000 个字符。下面是我自己的strlen的代码:

size_t  strlen(const char *c)
{
    int i;

    i = 0;
    while (c[i] != '\0')
        i++;
    return (i);
}

上面的 strlen 版本需要大约 2.1 秒来执行。通过不同的版本,我能够达到 ~1.4 秒。

我的问题是,为什么多个 if 语句比执行 while 循环更快?

size_t  strlen(const char *str)
{
    const char  *start;

    start = str;
    while (1)
    {
        if (str[0] == '\0')
            return (str - start);
        if (str[1] == '\0')
            return (str - start + 1);
        if (str[2] == '\0')
            return (str - start + 2);
        if (str[3] == '\0')
            return (str - start + 3);
        if (str[4] == '\0')
            return (str - start + 4);
        if (str[5] == '\0')
            return (str - start + 5);
        if (str[6] == '\0')
            return (str - start + 6);
        if (str[7] == '\0')
            return (str - start + 7);
        if (str[8] == '\0')
            return (str - start + 8);
        str += 9; // 
    }
}

我的问题是,为什么这么多 if 语句比仍然运行循环要快?

编辑:使用标准库,大约需要 1.25 秒。

【问题讨论】:

  • 因为您忘记在启用优化的情况下进行编译。
  • 1) 检查您的优化设置,2) 检查输出程序集,3) 我们需要一个体面的统计测试,其中包括您如何测量执行时间。 4)出于兴趣,标准库是否两次都击败(应该)?
  • 这类问题中的大约 90% 可以通过不正确的基准测试来回答。您甚至没有提及编译器和优化设置这一事实表明这是另一篇类似的帖子。
  • 您所做的是循环展开。可以使用-funroll-all-loops 使 GCC 展开第一个示例
  • 我发现 2.1 和 1.4 秒的 30k 个字符很难相信。 (在普通机器上,每个字符有数万个循环。)你忘了终止你的字符串吗?

标签: c performance for-loop performance-testing


【解决方案1】:

您的问题很中肯,但您的基准测试不完整且结果令人惊讶。

这是您的代码的修改和检测版本:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>

#define VERSION     3
#define TRIALS      100
#define ITERATIONS  100

#if VERSION == 1

size_t strlen1(const char *c) {
    size_t i;

    i = 0;
    while (c[i] != '\0')
        i++;
    return (i);
}
#define strlen(s)  strlen1(s)

#elif VERSION == 2

size_t strlen2(const char *str) {
    const char  *start;

    start = str;
    while (1) {
        if (str[0] == '\0')
            return (str - start);
        if (str[1] == '\0')
            return (str - start + 1);
        if (str[2] == '\0')
            return (str - start + 2);
        if (str[3] == '\0')
            return (str - start + 3);
        if (str[4] == '\0')
            return (str - start + 4);
        if (str[5] == '\0')
            return (str - start + 5);
        if (str[6] == '\0')
            return (str - start + 6);
        if (str[7] == '\0')
            return (str - start + 7);
        if (str[8] == '\0')
            return (str - start + 8);
        str += 9;
    }
}
#define strlen(s)  strlen2(s)

#elif VERSION == 3

size_t strlen3(const char *str) {
    const uint64_t *px, sub = 0x0101010101010101, mask = 0x8080808080808080;
    const char *p;

    for (p = str; (uintptr_t)p & 7; p++) {
        if (!*p)
            return p - str;
    }
    for (px = (const uint64_t *)(uintptr_t)p;;) {
        uint64_t x = *px++;
        if (((x - sub) & ~x) & mask)
            break;
    }
    for (p = (const char *)(px - 1); *p; p++)
        continue;
    return p - str;
}
#define strlen(s)  strlen3(s)

#endif

int get_next_line(int fd, char **pp) {
    char buf[32768];
    char *line = NULL, *new_line;
    char *p;
    ssize_t line_size = 0;
    ssize_t nread, chunk;

    while ((nread = read(fd, buf, sizeof buf)) > 0) {
        p = memchr(buf, '\n', nread);
        chunk = (p == NULL) ? nread : p - buf;
        new_line = realloc(line, line_size + chunk + 1);
        if (!new_line) {
            free(line);
            *pp = NULL;
            return 0;
        }
        line = new_line;
        memcpy(line + line_size, buf, chunk);
        line_size += chunk;
        line[line_size] = '\0';
        if (p != NULL) {
            lseek(fd, chunk + 1 - nread, SEEK_CUR);
            break;
        }
    }
    *pp = line;
    return line != NULL;
}

int main() {
    char *line = NULL;
    int fd, fd2, count, trial;
    clock_t min_clock = 0;

    fd = open("one_big_fat_line.txt", O_RDONLY);
    if (fd < 0) {
        printf("cannot open one_big_fat_line.txt\n");
        return 1;
    }

    fd2 = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE);
    if (fd2 < 0) {
        printf("cannot open output.txt\n");
        return 1;
    }

    for (trial = 0; trial < TRIALS; trial++) {
        clock_t t = clock();
        for (count = 0; count < ITERATIONS; count++) {
            lseek(fd, 0L, SEEK_SET);
            lseek(fd2, 0L, SEEK_SET);
            while (get_next_line(fd, &line) == 1) {
                write(fd2, line, strlen(line));
                write(fd2, "\n", 1);
                free(line);
            }
        }
        t = clock() - t;
        if (min_clock == 0 || min_clock > t)
            min_clock = t;
    }
    close(fd);
    close(fd2);

    double time_taken = (double)min_clock / CLOCKS_PER_SEC;
    printf("Version %d time: %.3f microseconds\n", VERSION, time_taken * 1000000 / ITERATIONS);
    return 0;
}

程序打开一个文件,使用自定义函数 read_next_line() 从其中读取行,该函数使用 unix 系统调用和 malloc 返回任意大小的行。然后,它使用 unix 系统调用 write 写入这些行,并使用单独的系统调用附加换行符。

使用您的测试文件(包含单行 ASCII 字符的 30000 字节文件)对该序列进行基准测试,显示出与您测量的非常不同的性能:取决于所选的 strlen 实现和编译优化设置,时间在我的笔记本电脑上,每次迭代的范围从 15 微秒到 82 微秒,正如您所观察到的那样,远不及 1 或 2 秒。

  • 使用 C 库默认实现,无论是否进行优化,我每次迭代的时间为 14.5 微秒。

  • 使用您的 strlen1 naive 实现,我得到 82 微秒(禁用优化)和 25 微秒(-O3 优化)。

  • 使用 strlen2 展开的实现,速度提高到 -O0 的 30 微秒和 -O3 的 20 微秒。

  • 最后,一次读取 8 个字节的更高级 C 实现 strlen3 进一步提高了性能,-O0 为 21 微秒,-O3 为 15.5 微秒。

请注意编译器优化对性能的影响远大于手动优化。

您的展开版本性能更好的原因是生成的代码每字节增加一次指针,并且每字节执行一次无条件跳转,而展开版本将这些减少到每 9 个字节一次。但是请注意,C 编译器使用 -O3 在幼稚代码上获得的性能与您自己展开循环所获得的性能几乎相同。

高级版在性能上与 C 库实现非常接近,可以使用带有 SIMD 指令的汇编语言。它一次读取 8 个字节并执行算术技巧来检测这些字节中的任何一个在从其值中减去 1 时,其最高位是否从 0 更改为 1。需要额外的初始步骤来对齐指针以读取 64 位字,从而避免在某些架构上具有未定义行为的未对齐读取。它还假设内存保护在字节级别不可用。在现代 x86 系统上,内存保护具有 4K 或更大的粒度,但其他一些系统(例如 Windows 2.x)的保护粒度要细得多,完全阻止了这种优化。

但请注意,基准测试还测量从输入文件读取、找到换行符并写入输出文件的时间。 strlenstrlen3 的相对性能可能要重要得多。实际上,针对 strlen(line) 和 30000 字节行的单独基准测试显示,strlen3() 的时间为 2.2 微秒,strlen() 的时间为 0.85 微秒。

结论:

  • 基准测试是一个棘手的游戏。
  • 编译器在被告知时擅长优化,-O3 是一个很好的默认值。
  • 重新定义库函数以尝试优化它们是徒劳且有风险的。

【讨论】:

  • 我们不能使用那些函数,我们基本上只允许使用 write、malloc 和 free,并且我们每次读取调用读取一个字符,所以这就是为什么它使用大字符串成本更高的原因。但是很好的答案!你能解释一下你的strlen吗?老实说,我对此一无所知,哈哈,对我来说太深了
  • @rogertaht:pastebin 不包含可编译程序的完整代码,因为缺少 ft_strlenft_strchrft_strnewft_strjoinft_strncpy 等实用函数。此外,这是您的代码还是学校图书馆?代码太复杂了。必须避免保持static t_list *l_files; 等隐藏静态状态:如果通过fd 读取的文件关闭并打开另一个文件,接收相同的句柄,静态列表将提供错误数据。
  • 编码风格看起来像 Epitech 或 42 强制执行的,恕我直言,可读性不强,也没有推广最佳实践,例如支持 while 而不是 for 循环......从头开始重写 C 库函数很有价值,但正确性和简单性必须是首要目标。
猜你喜欢
  • 2012-12-02
  • 2019-07-07
  • 2015-09-26
  • 2021-04-25
  • 2015-05-19
  • 1970-01-01
  • 2011-12-03
  • 2020-11-28
  • 1970-01-01
相关资源
最近更新 更多