为什么在Linux系统上,glibc库中的sscanf函数比fscanf函数慢得多?

34

我正在使用GCC 4.8和glibc 2.19在x86_64 Linux上。

在尝试不同的输入方法(另一个问题)时,我比较了fscanfsscanf。具体而言,我要么直接在标准输入上使用fscanf:

char s[128]; int n;

while (fscanf(stdin, "%127s %d", s, &n) == 2) { }

或者我会首先将整个输入读入缓冲区,然后使用sscanf遍历缓冲区。(将所有内容读入缓冲区需要极少的时间。)

char s[128]; int n;
char const * p = my_data;

for (int b; sscanf(p, "%127s %d%n", s, &n, &b) == 2; p += b) { }

令我惊讶的是,fscanf 版本明显更快。例如,使用 fscanf 处理数万行数据的时间如下:

10000       0.003927487 seconds time elapsed
20000       0.006860206 seconds time elapsed
30000       0.007933329 seconds time elapsed
40000       0.012881912 seconds time elapsed
50000       0.013516816 seconds time elapsed
60000       0.015670432 seconds time elapsed
70000       0.017393129 seconds time elapsed
80000       0.019837480 seconds time elapsed
90000       0.023925753 seconds time elapsed

现在同样适用于sscanf

10000       0.035864643 seconds time elapsed
20000       0.127150772 seconds time elapsed
30000       0.319828373 seconds time elapsed
40000       0.611551668 seconds time elapsed
50000       0.919187459 seconds time elapsed
60000       1.327831544 seconds time elapsed
70000       1.809843039 seconds time elapsed
80000       2.354809588 seconds time elapsed
90000       2.970678416 seconds time elapsed

我使用Google性能工具来测量。例如,对于50000行,fscanf代码需要大约50M周期,而sscanf代码需要大约3300M周期。因此,我使用perf record/perf report分解了顶级调用站点。使用fscanf

 35.26%  xf  libc-2.19.so         [.] _IO_vfscanf
 23.91%  xf  [kernel.kallsyms]    [k] 0xffffffff8104f45a
  8.93%  xf  libc-2.19.so         [.] _int_malloc

使用 sscanf 函数:

 98.22%  xs  libc-2.19.so         [.] rawmemchr
  0.68%  xs  libc-2.19.so         [.] _IO_vfscanf
  0.38%  xs  [kernel.kallsyms]    [k] 0xffffffff8104f45a

因此,使用sscanf时几乎所有的时间都花在了rawmemchr上!为什么会这样?fscanf代码如何避免这种代价?

我尝试搜索了一下,但最好的结果只是针对已锁定的realloc调用的讨论,但我认为这并不适用于这里。我还在想fscanf具有更好的内存局部性(反复使用同一缓冲区),但这并不能产生如此大的差异。

是否有人能够提供关于这种奇怪差异的任何见解?


fscanfsscanf的完整代码。 - Kerrek SB
我很难找到_IO_vfscanf的源代码。这个链接是我能找到的最好的,但那并不一定是glibc 2.19的。 - Kerrek SB
2
展示循环处理 - 看起来你遇到了一个“Schlemiel the Painter”问题。 - Michael Burr
@MichaelBurr:我已经链接了测试代码,并在问题中发布了循环。你认为sscanf每次都会扫描到字符串的末尾吗?这将与存储在b中的值相矛盾,该值具有预期值(即每次调用都会消耗一行输入)。 - Kerrek SB
1
@MichaelBurr:实际上,我认为Michael Burr是正确的,它似乎在整个文件中搜索尾随的null,然后解析出你想要的三个变量。请查看http://linux.die.net/man/3/rawmemchr上的示例。 - Mooing Duck
@MichaelBurr:他确实有,但是Schlemiel the Painter问题似乎在glibc本身而不是OP的代码中。 - R.. GitHub STOP HELPING ICE
2个回答

34

sscanf()将您传递的字符串转换为_IO_FILE*,以使字符串看起来像一个“文件”。这样就可以使用相同的内部_IO_vfscanf()函数来处理字符串和FILE*。

然而,在该转换的一部分中,在_IO_str_init_static_internal()函数中,它调用__rawmemchr (ptr, '\0'); 本质上是对您的输入字符串执行strlen()操作。每次调用sscanf()时都会进行此转换,由于输入缓冲区相当大,因此将花费相当多的时间计算输入字符串的长度。

使用fmemopen()从输入字符串创建FILE*并使用fscanf()可能是另一种选择。


16
我建议对glibc提交一个错误报告。原则上,通过使sscanf提供的虚拟FILE使用不需要预先知道字符串长度的自定义操作,可以解决这个问题。实际上,我们在musl libc中的实现避免了这个问题,所以我知道这是可能的。 :-) - R.. GitHub STOP HELPING ICE
@R..:我以前从未听说过musl——感谢你指出它! - Kerrek SB

8

看起来 glibc 的 sscanf() 在做任何其他操作之前会扫描源字符串的长度。

sscanf()(在 stdio-common/sscanf.c 中)实际上是对调用 _IO_vsscanf()(在 libio/iovsscanf.c 中)的一个包装。而 _IO_vsscanf() 做的第一件事就是通过调用 _IO_str_init_static_internal()(在 libio/strops.c 中)初始化自己的 _IO_strfile 结构体,如果没有提供则计算字符串的长度。


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