簡體   English   中英

為什么多個 if 語句比執行 while 循環更快?

[英]Why are multiple if statements faster than executing a while loop?

我的程序的輸入是一個大字符串,大約有 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 秒。

您的問題是相關的,但您的基准測試不完整並且結果令人驚訝。

這是您的代碼的修改和檢測版本:

#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寫入這些行,並使用單獨的系統調用附加換行符。

使用您的測試文件對這個序列進行基准測試,一個 30000 字節的文件,帶有一行 ASCII 字符,顯示出與您測量的非常不同的性能:根據所選的strlen實現和編譯優化設置,我的筆記本電腦上的時間范圍從每次迭代 15 微秒到 82 微秒,遠不及您觀察到的 1 或 2 秒。

  • 使用 C 庫默認實現,無論有沒有優化,每次迭代我都會得到 14.5 微秒。

  • 使用您的strlen1 naive 實現,禁用優化時我得到 82 微秒, -O3優化時得到 25 微秒。

  • 使用您的strlen2展開實現,速度提高到-O0 30 微秒和-O3 20 微秒。

  • 最后,更高級的 C 實現一次讀取 8 個字節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是一個很好的默認值。
  • 重新定義庫函數以嘗試優化它們是徒勞且有風險的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM