即使我将输出重定向到 /dev/null,printf 仍然会产生成本吗?
Posted
技术标签:
【中文标题】即使我将输出重定向到 /dev/null,printf 仍然会产生成本吗?【英文标题】:Will printf still have a cost even if I redirect output to /dev/null? 【发布时间】:2019-06-09 07:23:33 【问题描述】:我们有一个包含大量打印消息的守护进程。由于我们正在开发具有弱 CPU 和其他约束硬件的嵌入式设备,因此我们希望在最终版本中最小化 printf 消息的任何类型的成本(IO、CPU 等)。 (用户没有控制台)
我和我的队友意见不合。他认为我们可以将所有内容重定向到 /dev/null。它不会花费任何 IO,因此情感将是最小的。但我认为它仍然会消耗 CPU,我们最好为 printf 定义一个宏,这样我们就可以重写“printf”(也许只是返回)。
所以我需要一些关于谁是对的意见。 Linux 会足够聪明以优化 printf 吗?我真的很怀疑。
【问题讨论】:
小心副作用:printf("%d", x=a+b);
如果你重定向到 /dev/null 副作用会发生;如果你重写为 do nothing 宏,副作用会丢失
提供myprintf(...) return;
可能是您想要的。然后你可以有一个宏用于 printf 转发到该方法,保留副作用但不格式化任何字符串或调用 write
@pmg:printf
语句中的副作用是邪恶的。在代码审查中,我肯定会提出一个问题。
我会退后一步。在 Linux 守护进程中,有比 printf 更好的选项...例如,如果您在启动时设置日志级别(理想情况下来自环境变量),并且可以将日志定向到文件或另一台机器,请考虑使用 syslog。网络琐碎,日志级别允许您以相对较低的执行时间成本关闭您不关心的事情的日志记录。如果您执行诸如捕获一些信号之类的操作,您可以安排在运行时更改日志级别而无需停止,这样就更好了,更不用说重新编译守护程序了。
看来你需要一个合适的日志框架。至少,支持对消息进行惰性评估。
【参考方案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 是一种将 NSStrings 转换为 const char* 的 Objective-C 方法 - 你不需要。
【讨论】:
【参考方案2】:差不多。
当您将程序的标准输出重定向到/dev/null
时,对printf(3)
的任何调用仍将评估所有参数,并且字符串格式化过程仍将在调用write(2)
之前进行,后者会写入完整的格式化字符串到过程的标准输出。在内核级别,数据不会写入磁盘,而是被与特殊设备 /dev/null
关联的处理程序丢弃。
因此,在最好的情况下,您不会绕过或回避评估参数并将它们传递给printf
、printf
背后的字符串格式化作业以及至少一个系统调用来实际写入数据的开销,只需将标准输出重定向到/dev/null
。嗯,这在 Linux 上是一个真正的区别。该实现只返回您想要写入的字节数(由您调用write(2)
的第三个参数指定)并忽略其他所有内容(请参阅this answer)。根据您写入的数据量和目标设备(磁盘或终端)的速度,性能差异可能会有很大差异。在嵌入式系统上,一般来说,通过重定向到/dev/null
来切断磁盘写入可以为大量写入数据节省相当多的系统资源。
虽然理论上,该程序可以检测到/dev/null
并在它们遵守的标准(ISO C 和 POSIX)的限制范围内执行一些优化,但基于对常见实现的一般理解,它们实际上不会(即我是不知道有任何 Unix 或 Linux 系统这样做)。
POSIX 标准要求将任何对printf(3)
的调用写入标准输出,因此根据关联的文件描述符抑制对write(2)
的调用是不符合标准的。有关 POSIX 要求的更多详细信息,您可以阅读Damon's answer。哦,还有一个简短的说明:所有 Linux 发行版实际上都符合 POSIX,尽管没有认证。
请注意,如果您完全替换 printf
,一些副作用可能会出错,例如 printf("%d%n", a++, &b)
。如果您确实需要根据程序执行环境抑制输出,请考虑设置一个全局标志并包装 printf 以在打印前检查该标志 - 它不会减慢程序到性能损失可见的程度,因为单个条件检查比调用 printf
并执行所有字符串格式化要快得多。
【讨论】:
这样的答案应该说明它们是基于对常见实现的一般理解,而不是基于特定文档。从理论上讲,C 实现没有理由不检查stdout
,知道它是/dev/null,并抑制不包含%n
且未使用其返回值的printf
调用。我们不能真的断言没有人这样做过,学生应该了解信息的来源,因为工程学的一个重要部分是知道你如何知道某事(它是否在标准中指定,是否只是假设,是否可证明,等等)。
@EricPostpischil 谢谢!非常有价值的信息。
@EricPostpischil 请参阅Damon's answer 以获取 POSIX 标准的参考 - 禁止通过 write(2)
调用写入标准输出。
AFAIK,控制台输出非常慢,因此写入 /dev/null 节省的时间可能很重要。不过,如果你是写文件,效率会降低。
@SJuan76 写入终端并不一定很慢,显示确实很慢(即取决于终端)。【参考方案3】:
编写你自己的包装 printf() 使用 printf() 源作为指导,如果设置了 noprint 标志,则立即返回。这样做的缺点是实际打印时会消耗更多资源,因为必须解析格式字符串两次。但它在不打印时使用的资源可以忽略不计。不能简单地替换 printf(),因为 printf() 中的底层调用可能会随着新版本的 stdio 库而改变。
void printf2(const char *formatstring, ...);
【讨论】:
如果您在printf2
中使用vprintf
,您将不必解析格式字符串两次。【参考方案4】:
printf
函数将写入stdout
。它不符合优化/dev/null
。
因此,您将承担解析格式字符串和评估任何必要参数的开销,并且您将拥有至少一个系统调用,而且您将复制一个缓冲区到内核地址空间(与系统调用的成本相比,这可以忽略不计) .
此答案基于 POSIX 的具体文档。
系统接口 dprintf, fprintf, printf, snprintf, sprintf - 打印格式化输出
fprintf() 函数应将输出放在指定的输出流上。 printf() 函数应将输出放在标准输出流 stdout 上。 sprintf() 函数应将输出后跟空字节“\0”放在从 *s 开始的连续字节中;用户有责任确保有足够的可用空间。
基本定义 应该 对于符合 POSIX.1-2017 的实现,描述强制性的功能或行为。应用程序可以依赖于特性或行为的存在。
【讨论】:
内核负责将缓冲区复制到内核地址空间,而 null 驱动程序可能省略了该步骤(在 Linux 上,确实如此 - 它甚至不检查这是一个有效的地址) 也许更清楚一点,不是每次调用 printf 都会导致系统调用 - 目前这有点模棱两可,否则很好的答案。 -1,这会忽略 as-if 规则(适用于整个实现,而不仅仅是编译器)。如果整个实现可以证明可观察行为没有差异,它可以进行任何它喜欢的优化,即使在编译完成后也是如此。现在,如果我们谈论的是 C 以外的语言,那您可能是对的。 @Kevin C 标准的 as-if 规则是否也适用于 POSIX 标准? @Kevin 由于对 I/O 函数的调用本身就是可观察的行为,因此不受优化限制。如果标准规定只有 I/O 是可观察的效果,那么将 printf 删除到 /dev/null 是可能的。【参考方案5】:一般来说,如果它们不影响程序的可观察(功能)输出,则允许实现执行此类优化。在printf()
的情况下,这意味着如果程序不使用返回值,并且如果没有%n
转换,那么实现将被允许什么都不做。
实际上,我不知道目前(2019 年初)在 Linux 上执行此类优化的任何实现 - 我熟悉的编译器和库将格式化输出并将结果写入 null 设备,依赖在内核上'忽略它。
如果您确实需要在不使用输出时节省格式化成本,您可能需要编写自己的转发函数 - 您希望它返回void
,并且您应该检查格式字符串为%n
。 (如果您需要这些副作用,您可以将 snprintf
与 NULL
和 0
缓冲区一起使用,但节省的费用不太可能偿还所付出的努力。
【讨论】:
既然抽象机的可观察行为包括对库 I/O 函数的调用,那么至少printf
肯定会被调用,对吧?虽然我不确定对库函数的两个不同调用如何被认为是不同的......【参考方案6】:
printf
函数写入stdout
。如果连接到stdout
的文件描述符被重定向到/dev/null
,则不会在任何地方写入任何输出(但仍会写入),但对printf
本身的调用以及它所做的格式化仍然会发生。
【讨论】:
@OP:补充:您可以通过创建提供新FILE *
的新驱动程序来进一步降低printf()
的成本(取决于您的平台是否支持)。在这种情况下,您可以创建一个丢弃数据的数据接收器。格式化等的成本仍然存在,但操作系统调用写入/dev/null
消失了。
这样的答案应该说明它们是基于对常见实现的一般理解,而不是基于特定文档。从理论上讲,C 实现没有理由不检查 stdout
,知道它是 /dev/null,并抑制不包含 %n
且未使用其返回值的 printf
调用。我们不能真的断言没有人这样做过,学生应该了解信息的来源,因为工程学的一个重要部分是知道你如何知道某事(它是否在标准中指定,是否只是假设,是否可证明,等等)。以上是关于即使我将输出重定向到 /dev/null,printf 仍然会产生成本吗?的主要内容,如果未能解决你的问题,请参考以下文章