printf会拖慢我的程序

29

我有一个小的C程序用于计算哈希(用于哈希表)。代码看起来相当干净,但有一些与之无关的问题困扰着我。

我可以在约0.2-0.3秒内轻松生成大约100万个哈希值(使用/usr/bin/time进行基准测试)。然而,当我在for循环中使用printf()打印它们时,程序会变慢,大约需要5秒钟。

  1. 为什么会这样?
  2. 如何使它更快?可能是mmapp() stdout?
  3. stdlibc在这方面的设计如何,如何改进?
  4. 内核如何更好地支持它?需要如何修改才能使本地“文件”(套接字、管道等)的吞吐量真正快速?

我期待有趣和详细的回复。谢谢。

PS:这是用于编译器构建工具集,所以请不要羞于深入细节。虽然这与问题本身无关,但我想指出细节让我感兴趣。

附加说明

我正在寻找更多的程序化方法来解决问题并进行解释。确实,管道可以完成工作,但我无法控制“用户”所做的事情。

当然,我现在正在进行测试,这不是“普通用户”会执行的。但这并不改变一个简单的printf()减慢进程速度的事实,这是我试图找到最佳程序化解决方案的问题。


补充说明 - 令人惊讶的结果

参考时间是在TTY内使用普通printf()调用,并且大约需要4分20秒。

在/dev/pts(例如Konsole)下进行测试可将输出加速约5秒。

在我的测试代码中使用setbuffer()设置为16384大小几乎需要相同的时间,对于8192也是如此:约6秒。

当使用 setbuffer() 时,显然没有任何影响:它花费的时间相同(在 TTY 上大约 4 分钟,在 PTS 上大约 5 秒)。

令人惊讶的是,如果我在 TTY1 上开始测试,然后切换到另一个TTY,它所花费的时间和在 PTS 上一样:大约 5 秒。

结论:内核做了一些与可访问性和用户友好性有关的事情。哎呀!

通常情况下,无论你是否在活动的 TTY 上盯着它看,或者切换到另一个 TTY,它都应该同样缓慢。


教训:运行输出密集型程序时,请切换到另一个 TTY!


2
如果你将输出重定向到 /dev/null,那么你的程序速度会有多快? - Erich Kitzmueller
1
这不是一个“简单”的问题。I/O通常比直接的CPU计算和总线操作慢上数个数量级,认识到这一点并不令人惊讶。 - Michael Foukarakis
4
Flavius说,这是因为当TTY显示时,每个新行都需要向上滚动整个屏幕。屏幕上的每个字符单元都映射到屏幕缓冲区中的特定位置,因此移动字符意味着在屏幕缓冲区中移动字节。在一个80列的控制台上,将24行向上移动实际上相当于对于您输出的每一行进行了近2k的memmove操作。请注意,此处提到的“memmove”是一种内存操作函数。 - caf
@caf:虽然我知道这个,但我没有意识到。感谢您的精彩解释 :) - Flavius
当您在 TTY 上运行时,它既是输出设备又是输入设备。当您输出字符时,它会查看是否键入了像 CTRL C、CTRL S/Q 这样的键来停止显示。这可能是为什么需要这么长时间... 或者至少有助于减慢速度。 - AnthonyLambert
显示剩余6条评论
9个回答

35

非缓冲输出非常慢。

默认情况下,stdout 是完全缓冲的,但是当连接到终端时,stdout 要么是非缓冲的,要么是行缓冲的。

尝试使用 setvbuf() 打开 stdout 的缓冲区,像这样:

char buffer[8192];

setvbuf(stdout, buffer, _IOFBF, sizeof(buffer));

哦,printf()默认写入stdout。我不会干扰printf()的工作方式。 - Flavius

15

你可以将字符串存储在缓冲区中,并在结束时或定期地将它们输出到文件(或控制台),当缓冲区已满时进行输出。

如果要输出到控制台,滚动通常会影响性能。


4
+1,特别是对于滚动。想象一下在滚动过程中需要进行的所有位块复制和位图复制操作... - sleske
2
你的回复让我在一个干净的TTY下测试了程序,并在Konsole的管理PTS下进行了测试。结果是:Konsole加快了速度!从TTY运行程序花了4分钟20秒(我认为这应该作为真正的测试参考),从PTY运行只用了5秒。 - Flavius
1
滚动又加一分。在GNU screen中运行一些健谈的程序(然后分离它)会大大加快速度! - Lester Cheung

9
如果你在控制台上使用printf()输出,通常会非常慢。我不确定为什么,但我认为它不会返回直到控制台图形显示输出的字符串。此外,你不能将mmap()映射到stdout。
写入文件应该更快(但仍然比计算哈希慢几个数量级,所有I/O都很慢)。

7
您可以尝试将shell中的输出从控制台重定向到文件。使用此方法,只需几秒钟就可以创建出大小为几GB的日志文件。

7
  1. 与直接计算相比,I/O 总是较慢的。系统必须等待更多组件可用才能使用它们。然后它必须等待响应才能继续执行。相反,如果它只是进行计算,那么它只需要在 RAM 和 CPU 寄存器之间移动数据。

  2. 我没有测试过这个方法,但将哈希值附加到字符串上,然后在最后只打印字符串可能会更快。不过,如果你使用的是 C 而不是 C++,这可能会很麻烦!

3 和 4 对我来说太难了,恐怕无法翻译。


4
  1. 为什么不在需要时再创建字符串,而不是在构造时创建呢?一秒钟输出40屏数据是没有意义的,你怎么可能看得过来呢?为什么不按需创建输出,并只显示最后一屏满屏幕,然后根据需要用户滚动呢?

  2. 为什么不使用sprintf将内容打印到一个字符串中,然后在内存中构建所有结果的连接字符串,最后再打印出来呢?

  3. 通过切换到sprintf,您可以清楚地看到花费在格式转换上的时间和花费在向控制台显示结果上的时间,并相应地更改代码。

  4. 控制台输出本质上是慢的,创建哈希只涉及少量内存字节的操作。控制台输出需要经过许多层操作系统,这些层会有处理线程/进程锁定等的代码。最终它才会传递到显示驱动程序,该驱动程序可能是一个9600波特率设备!或大型位图显示器,简单的屏幕滚动功能可能涉及操作数兆字节的内存。


1
关于(4):我明白,但如果我是一个操作系统的开发者,是否可以将输出从一个位置复制到另一个位置/进程?如果可以,你认为我该如何做才能提高速度? - Flavius
1
在早期的游戏开发中,程序员们通常直接操作输出设备,例如将字符直接写入显示内存。然而,如今即便是他们,也大多使用库来与硬件进行通信,这样可以实现设备独立且利用硬件加速。今天很少需要绕过这些层来进行操作。 - AnthonyLambert

4

由于I/O操作通常比CPU计算慢得多,因此您可能希望首先将所有值存储在最快的I/O中。如果有足够的RAM,则使用RAM;如果没有,则使用文件,但这比RAM慢得多。

现在可以通过另一个线程事后或并行地打印出这些值。因此,计算线程不需要等待printf返回。


4
我很早以前发现了一个技巧,使用这个技巧,本应该显而易见的事情。不仅I/O速度慢,特别是控制台输出,而且十进制数字格式化也不快。如果你可以将数字以二进制形式放入大缓冲区中,并将其写入文件,你会发现它要快得多。
此外,谁会读取它们呢?如果没有人需要阅读所有内容,那么将它们全部打印成人类可读的格式就没有意义。

2
我猜终端类型使用了一些缓冲输出操作,所以当你执行printf时,它不会在分秒钟内输出,而是存储在终端子系统的缓冲内存中。这可能受到其他影响因素的影响,例如除你的程序外还有一个占用大量内存的操作正在运行。简而言之,有太多的事情同时发生,如分页、交换、另一个进程的重负载I/O、内存利用的配置、可能的内存升级等等。
最好将字符串连接起来,直到达到某个限制,然后一次性写出来。或者甚至使用pthread来执行所需的进程执行。
至于第2和3点,我无法解决。对于第4点,我不熟悉Sun,但知道并且已经玩过Solaris,可能有一个内核选项可以使用虚拟tty。我承认已经有一段时间没有处理内核配置并重新编译它了。因此我的记忆可能不太好,可以在选项中搜索一下。
用户@主机:/usr/src/linux $ make; make menuconfig **OR kconfig if from X**
这将启动内核菜单,请在设备子树下查看视频设置部分。
编辑:但是有一个调整可以通过将文件添加到proc文件系统(如果存在这样的东西)或可能传递到内核的开关来完成,类似于这个(这是想象的,不意味着它实际存在),fastio。
希望对你有所帮助, 最好的问候, 汤姆。

谢谢您的答复。正如您在问题标签中所看到的,这是一台Linux机器。 - Flavius
@Flavius:抱歉,我之前在编辑答案时看到了Sun和Solaris的内容,但可能是与其他SO线程混淆了...对此表示歉意。 - t0mm13b

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