【问题标题】:Why is glibc's sscanf vastly slower than fscanf on Linux?为什么 glibc 的 sscanf 在 Linux 上比 fscanf 慢得多?
【发布时间】:2014-07-18 09:40:20
【问题描述】:

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

在为a different question 使用不同的输入法时,我比较了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 代码如何避免这种成本?

我尝试搜索此内容,但我能想到的最好的结果是 this discussion 锁定的 realloc 调用,我认为这里不适用。我还认为fscanf 具有更好的内存局部性(一遍又一遍地使用相同的缓冲区),但这不会产生如此大的差异。

有人对这种奇怪的差异有任何见解吗?

【问题讨论】:

  • 完整代码:fscanf, sscanf
  • 我找不到_IO_vfscanf 的源代码。 This 是我能找到的最好的,但不一定是 glibc 2.19。
  • 显示循环处理 - 看起来你有一个"Schlemiel the Painter" problem
  • @MichaelBurr:我链接了测试代码,并在问题中发布了循环。你认为sscanf 每次都会扫描到字符串的末尾吗?这将与存储在 b 中的值相矛盾,后者具有预期值(即,每次调用都会消耗一行输入)。
  • @MichaelBurr:其实我觉得Michael Burr 是对的,看来sscanf 正在整个文件 中搜索尾随的null,然后解析出三个变量你要。看linux.die.net/man/3/rawmemchr上的例子

标签: c performance glibc scanf


【解决方案1】:

sscanf() 将您传入的字符串转换为_IO_FILE*,使字符串看起来像一个“文件”。这样同一个内部 _IO_vfscanf() 可以同时用于字符串和 FILE*。

但是,作为转换的一部分,在 _IO_str_init_static_internal() 函数中完成,它在您的输入字符串上调用 __rawmemchr (ptr, '\0'); 本质上是 strlen() 调用。每次调用 sscanf() 时都会进行此转换,并且由于您的输入缓冲区相当大,因此会花费大量时间来计算输入字符串的长度。

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

【讨论】:

  • 我建议提交一份针对 glibc 的错误报告。通过使sscanf 提供的虚拟FILE 使用不需要预先了解字符串长度的自定义操作,原则上肯定可以解决此问题。实际上我们在 musl libc 中的实现避免了这个问题,所以我知道这是可能的。 :-)
  • @R..:我以前从未听说过 musl -- 感谢您指出!
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2020-04-29
    • 2014-10-16
    • 1970-01-01
    • 2013-08-18
    • 1970-01-01
    • 2017-12-26
    • 2011-01-25
    相关资源
    最近更新 更多