如果我将输出重定向到 /dev/null,printf 还会产生费用吗?

63
我们有一个包含很多打印消息的守护程序。由于我们正在使用CPU较弱和其他约束硬件的嵌入式设备,因此我们希望在最终版本中尽量减少printf消息的任何成本(IO、CPU等)。(用户没有控制台)
我的队友和我意见不一。他认为我们可以将所有内容重定向到/dev/null。这样不会产生任何IO成本,所以影响将是最小的。但我认为这仍然会耗费CPU,我们最好定义一个printf的宏,这样我们就可以重新编写"printf"(可能只是返回)。
因此,我需要一些关于谁是正确的意见。Linux会聪明到足以优化printf吗?我真的很怀疑。

18
注意副作用: printf("%d", x=a+b); 如果你重定向到 /dev/null,会发生副作用; 如果你将其改写为一个 do nothing 宏,那么就会失去副作用。 - pmg
5
提供一个myprintf(...) { return; }可能是你想要的。然后你可以有一个宏来将printf转发到该方法,保留副作用但不格式化任何字符串或调用write。 - msrd0
15
@pmg:在 printf 语句中使用副作用是有害的。在代码审查中,我肯定会提出问题。 - MSalters
8
我会退一步。在Linux守护进程中,有比printf更好的选择... 例如,考虑使用syslog,如果您在启动时设置日志级别(最好从环境变量中),您可以将日志轻松地定向到文件或另一台机器上,而且日志级别允许您在执行时间成本相对较低的情况下关闭不关心的事物的记录。如果像捕获一些信号这样做得更好,您可以安排在运行时更改日志级别,而无需停止,更不用重新编译守护程序。 - Dan Mills
4
看起来你需要一个合适的日志框架。最起码,需要支持对消息进行惰性求值的框架。 - Alexander
显示剩余7条评论
6个回答

74
基本上是这样的。当你将程序的标准输出重定向到/dev/null时,任何对printf(3)的调用仍会评估所有参数,并且在调用write(2)之前仍然进行字符串格式化处理,该函数将完整格式化的字符串写入进程的标准输出。在内核级别上,数据不会写入磁盘,而是由与特殊设备/dev/null关联的处理程序丢弃。因此,在最好的情况下,仅通过将stdout重定向到/dev/null,您将无法绕过或规避评估参数并将它们传递给printfprintf背后的字符串格式化工作以及至少一次实际写入数据的系统调用的开销。嗯,这在Linux上是有真正的区别的。该实现仅返回您想要写入的字节数(由调用write(2)的第三个参数指定),并忽略其他所有内容(请参见此答案)。根据您要写入的数据量以及目标设备(磁盘或终端)的速度,性能差异可能会很大。通常情况下,在嵌入式系统中,通过重定向到/dev/null切断磁盘写入可以为大量写入的数据节省相当多的系统资源。

虽然在理论上,程序可以检测到 /dev/null 并在它们遵循的标准(ISO C 和 POSIX)的限制下执行一些优化,但基于对常见实现的通识了解,它们实际上不会这样做(即我不知道有任何 Unix 或 Linux 系统这样做)。

POSIX 标准强制要求对于任何对 printf(3) 的调用都要写入标准输出,所以根据相关文件描述符抑制调用 write(2) 不符合标准规范。关于 POSIX 要求的更多细节,您可以阅读 Damon 的回答。哦,还有一个快速提醒:尽管没有得到认证,所有 Linux 发行版实际上都是符合 POSIX 标准的。

请注意,如果您完全替换 printf,可能会出现一些副作用错误,例如 printf("%d%n", a++, &b)。如果您确实需要根据程序执行环境抑制输出,请考虑设置一个全局标志并包装 printf 以在打印之前检查该标志 - 它不会使程序变慢到可以看到性能损失的程度,因为单个条件检查比调用 printf 并做所有字符串格式化要快得多。


31
这样的答案应该说明它们基于常见实现的一般理解,而不是特定文档。从理论上讲,C语言实现可能会检查stdout,了解它是否为/dev/null,并抑制那些不包含%n且返回值未被使用的printf调用。我们无法肯定没有人这样做过,学生应该了解信息来源,因为工程学的一个重要部分就是知道你知道某些东西的方式(它是否在标准中规定,是否只是默认的,是否可以证明等等)。 - Eric Postpischil
1
@EricPostpischil 谢谢您!非常有价值的信息。 - iBug
9
请参考 Damon 的回答 ,该回答来自 POSIX 标准,禁止抑制 write(2) 调用以写入标准输出。请注意不要改变原意,仅使翻译内容更加通俗易懂。 - iBug
4
据我所知,控制台输出速度较慢,因此将内容写入/dev/null可以节省大量时间。但如果您要将内容写入文件,则效果会不太明显。 - SJuan76
3
写入终端并不一定慢,显示 内容才会慢(即取决于终端)。 - iBug
显示剩余10条评论

41

printf函数将写入到标准输出stdout,不遵循优化/dev/null的规范。因此,您需要解析格式字符串并评估任何必要的参数,至少需要一个系统调用,并将缓冲区复制到内核地址空间(与系统调用的成本相比是可以忽略不计的)。

这个答案是基于POSIX具体的文档。

系统接口
dprintf、fprintf、printf、snprintf、sprintf-打印格式化输出

fprintf()函数应将输出放置在指定的输出流上。printf()函数应将输出放置在标准输出流stdout上。sprintf()函数应将输出和空字节'\0'放置在从*s开始的连续字节中;用户有责任确保有足够的空间可用。

基本定义
应该
对于符合POSIX.1-2017的实现,描述了强制执行的功能或行为。应用程序可以依赖于该功能或行为的存在。


2
内核负责将缓冲区复制到内核地址空间,而空驱动程序可能省略了这一步骤(在Linux上,它肯定会 - 它甚至不检查它是否是有效地址)。 - Random832
11
-1,这忽略了“虚拟等价原则(as-if rule)”(适用于整个实现而不仅仅是编译器)。如果整个实现可以证明没有可观测行为上的区别,那么它可以进行任何优化,甚至在编译完成后进行。现在,如果我们讨论的是C以外的语言,你可能是正确的。 - Kevin
5
@Kevin:C语言标准中的"as-if"规则是否也适用于POSIX标准? - Angew is no longer proud of SO
2
@Kevin:不,它说的是“与...对齐”,那是不同的。相反地:针对C编程语言的语言相关服务指南中指出:“寻求使用ISO C标准来声明符合性的实现者应按照POSIX符合性的描述来声明符合性”。 - Damon
1
@jinawee:不,它们不是。可观察行为包括1.访问易失性变量,2.写入文件的数据,3.写入“交互设备”(ttys)的数据,以及4. FPU模式。写入/dev/null的数据不属于这四种情况之一。 - Kevin
显示剩余5条评论

11

printf 函数会写入到标准输出流 (stdout)。如果连接到 stdout 的文件描述符被重定向到 /dev/null,则不会在任何地方输出 (但仍然会被写入),但是调用 printf 本身和其格式化的内容仍将发生。


1
@OP:另外,您可以通过创建一个新的驱动程序来进一步降低printf()的成本,该驱动程序提供一个新的FILE *(取决于您的平台是否支持)。在这种情况下,您可以创建一个数据池,用于丢弃数据。格式化等成本仍然存在,但是写入/dev/null的操作系统调用消失了。 - glglgl
2
这样的答案应该说明它们基于通用实现的一般理解,而不是具体文档。从理论上讲,C实现可能会检查stdout,了解它是/dev/null,并抑制不包含%n且返回值未使用的printf调用。我们无法真正断言没有人这样做,学生们应该学习信息的来源,因为工程的重要部分是知道您如何获得某些信息(它是否在标准中指定,是否仅仅是假设,是否可证明等)。 - Eric Postpischil

5

编写自己的printf()函数,以printf()源代码为指南,并在设置了noprint标志后立即返回。 缺点是实际打印时由于需要解析格式字符串两次,会消耗更多资源。 但在不打印时使用的资源微不足道。不能简单地替换printf(),因为stdio库的新版本中printf()内部调用可能会改变。

void printf2(const char *formatstring, ...);


5
如果在printf2函数中使用vprintf,你就不需要两次解析格式字符串。 - zwol

4
一般来说,如果实现不会影响程序的可观察(功能性)输出,则可以执行此类优化。在 printf() 的情况下,这意味着如果程序不使用返回值,并且没有 %n 转换,则允许实现什么都不做。
实际上,在 Linux 上,我不知道任何当前(2019 年初)执行此类优化的实现 - 我熟悉的编译器和库将格式化输出并将结果写入空设备,依赖于内核忽略它。
如果您真的需要节省未使用输出时的格式化成本,您可能需要编写自己的转发函数 - 您需要它返回 void,并且应该检查格式字符串是否包含 %n。(如果需要这些副作用,您可以使用带有 NULL0 缓冲区的 snprintf,但是节省的效果不大)。

由于抽象机的可观察行为包括对库I/O函数的调用,所以至少需要调用printf,是吗?虽然我不确定两个不同的库函数调用是否被认为是不同的... - jinawee

1
在C语言中,写入0;将执行无操作,类似于;
这意味着你可以编写一个宏像这样:
#if DEBUG
#define devlognix(frmt,...) fprintf(stderr,(frmt).UTF8String,##__VA_ARGS__)
#else
#define nadanix 0
#define devlognix(frmt,...) nadanix
#endif
#define XYZKitLogError(frmt, ...) devlognix(frmt)

在这里,XYZKitLogError 将是您的日志命令。甚至可以这样说。

#define nadanix ;

该方法会在编译时踢出所有日志调用,并用0;;替换,以便被解析掉。

您将收到未使用变量警告,但它做了您想要的事情,而这种副作用甚至可以帮助您,因为它告诉您在发布中不需要的计算。

.UTF8String是将NSString转换为const char*的Objective-C方法 - 您不需要使用它。


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