我的程序的输入是一个大字符串,大约30,000个字符。下面是我自己的代码:
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语句比循环运行还要快?
编辑:使用stantard lib大约需要1.25秒。
答案 0 :(得分: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
编写这些行,并在换行后附加一个单独的系统调用。
以测试文件为基准,该序列是一个30000字节的文件,其中包含一行ASCII字符,显示出与您所测量的性能完全不同的性能:根据strlen
的选定实现方式和编译优化设置,在我的笔记本电脑上,每次迭代的时间范围从15微秒到82微秒,与您观察到的相差1到2秒。
使用C库的默认实现,无论有没有优化,每次迭代都可以得到14.5微秒。
使用您的strlen1
天真实施,禁用优化后我得到82微秒,而-O3
优化时我得到25微秒。
使用您的strlen2
展开的实施,-O0
的速度提高到30微秒,-O3
的速度提高到20微秒。
最后,一次更高级的C实现一次{8}读取8个字节,strlen3
可以进一步提高性能,而-O0
可以达到15.5微秒。
请注意,与手动优化相比,编译器优化对性能的影响要大得多。
展开版本的性能更好的原因是,生成的代码每字节增加一次指针,并且每字节执行一次无条件跳转,而展开版本将指针减少到每9字节一次。但是请注意,C编译器在-O3
的天真代码上获得的性能几乎与您自己展开循环的性能相同。
高级版本的性能与C库实现非常接近,C库实现可能使用带有SIMD指令的汇编语言。它一次读取8个字节,并执行算术技巧以检测从其值中减去-O3
时,这些字节中的任何一个最高位是否从0
变为1
。需要额外的初始步骤来对齐指针以读取64位字,从而避免在某些体系结构上具有未定义行为的未对齐读取。它还假定在字节级别上不提供内存保护。在现代x86系统上,内存保护的粒度为4K或更大,但是其他一些系统(例如Windows 2.x)的保护粒度则要小得多,从而完全阻止了这种优化。
但是请注意,基准测试还测量从输入文件读取,定位换行符和写入输出文件的时间。 1
和strlen
的相对性能可能要重要得多。确实,仅strlen3
的30000字节行的基准显示strlen(line)
的时间为2.2微秒,strlen3()
的时间为0.85微秒。
结论:
strlen()
是一个很好的默认值。