为什么glibc';在Linux上,s sscanf比fscanf慢得多?
我在x86_64 Linux上使用GCC 4.8和glibc 2.19 在使用不同的输入法时,我比较了为什么glibc';在Linux上,s sscanf比fscanf慢得多?,c,performance,glibc,scanf,C,Performance,Glibc,Scanf,我在x86_64 Linux上使用GCC 4.8和glibc 2.19 在使用不同的输入法时,我比较了fscanf和sscanf。具体来说,我会直接在标准输入上使用fscanf: char s[128]; int n; while (fscanf(stdin, "%127s %d", s, &n) == 2) { } 或者我会先将整个输入读入缓冲区,然后使用sscanf遍历缓冲区。(将所有内容读入缓冲区只需很少的时间。) 令我惊讶的是,fscanf版本的速度要快得多。例如,使用fsc
fscanf
和sscanf
。具体来说,我会直接在标准输入上使用fscanf
:
char s[128]; int n;
while (fscanf(stdin, "%127s %d", s, &n) == 2) { }
或者我会先将整个输入读入缓冲区,然后使用sscanf
遍历缓冲区。(将所有内容读入缓冲区只需很少的时间。)
令我惊讶的是,fscanf
版本的速度要快得多。例如,使用fscanf
处理数万行需要这么长的时间:
10000 0.003927487秒经过的时间
20000 0.006860206秒经过的时间
30000 0.007933329秒经过的时间
40000 0.012881912秒经过时间
50000 0.013516816秒经过的时间
60000 0.015670432秒经过时间
70000 0.017393129秒经过的时间
80000 0.019837480秒经过的时间
90000.023925753秒经过的时间
现在与sscanf
相同:
10000 0.035864643秒
20000 0.127150772秒经过的时间
30000 0.319828373秒经过的时间
40000 0.611551668秒经过的时间
50000 0.919187459秒经过的时间
60000 1.327831544秒经过的时间
70000 1.809843039秒经过的时间
80000 2.354809588秒经过的时间
90000 2.970678416秒经过的时间
我用谷歌性能工具来衡量这一点。例如,对于50000条线路,fscanf
代码需要大约50M的循环,而sscanf
代码需要大约3300M的循环。因此,我用perf-record
/perf-report
分析了排名靠前的呼叫站点。使用fscanf
:
35.26%xf libc-2.19.so[.]\u IO\u vfscanf
23.91%xf[kernel.kallsyms][k]0xffffffff8104f45a
8.93%xf libc-2.19.so[.]\u int\u malloc
使用sscanf
:
98.22%xs libc-2.19.so[.]rawmemchr
0.68%xs-libc-2.19.so[.]\u IO\u vfscanf
0.38%xs[kernel.kallsyms][k]0xffffffff8104f45a
因此,几乎所有使用sscanf
的时间都花在rawmemchr
上!为什么会这样?fscanf
代码如何避免这种成本
我试着搜索这个,但我能想到的最好的方法是锁定realloc
调用,我认为这在这里不适用。我还认为,fscanf
具有更好的内存局部性(反复使用相同的缓冲区),但这并不能产生如此大的差异
有人对这种奇怪的差异有什么见解吗?看起来glibc的
sscanf()
在做任何其他事情之前先扫描源字符串的长度
sscanf()
(在stdiocommon/sscanf.c
中)本质上是对\u IO\u vsscanf()
(在libio/iovsscanf.c
中)调用的包装。\u IO\u vsscanf()
做的第一件事是通过调用\u IO\u str\u init\u static\u internal()
(在libio/strops.c
中)来初始化它自己的\u IO\u strfile
结构,如果没有提供,它将计算字符串的长度。sscanf()将传入的字符串转换为\u IO\u文件*
,使字符串看起来像一个“文件”。因此,相同的内部_IO_vfscanf()可用于字符串和文件*
然而,作为转换的一部分,在一个_IO_str_init_static_internal()函数中完成,它调用u rawmemchr(ptr,“\0”)
本质上是对输入字符串的strlen()调用。这种转换在每次调用sscanf()时都会完成,因为您的输入缓冲区相当大,它将花费相当多的时间来计算输入字符串的长度
使用fmemopen()和使用fscanf()从输入字符串创建文件*可能是另一种选择。完整的代码:,我很难找到
\u IO\u vfscanf
的源代码。是我能找到的最好的,但这不一定是glibc 2.19。显示循环处理-看起来你有一个循环。@MichaelBurr:我链接了测试代码,并在问题中发布了循环。您认为sscanf
每次都会扫描到字符串的末尾吗?这将与存储在b
中的值相矛盾,该值具有预期值(即每次调用消耗一行输入)。@MichaelBurr:事实上,我认为Michael Burr是对的,似乎sscanf
正在搜索整个文件中的尾随空,然后解析出您想要的三个变量。看看上面的例子,我建议针对glibc提交一份bug报告。通过使sscanf
提供的虚拟文件
使用不需要预先了解字符串长度的自定义操作,原则上可以解决此问题。实际上,我们在musl libc中的实现避免了这个问题,所以我知道这是可能的。:-)@R:我以前从未听说过musl——谢谢你指出它!
char s[128]; int n;
char const * p = my_data;
for (int b; sscanf(p, "%127s %d%n", s, &n, &b) == 2; p += b) { }