多次 printf() 调用与一次使用长字符串调用 printf() ?

问题描述 投票:0回答:5

假设我有一行

printf()
,带有一长串:

printf( "line 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"      
"line 8\n"
"line 9\n.. etc");  

与每行有多个

printf()
相比,这种风格会产生哪些成本?
如果字符串太长会不会出现栈溢出的情况?

c string printf
5个回答
17
投票

与每行使用多个 printf() 相比,这种风格产生的成本是多少?

多个

printf
将导致多个函数调用,这是唯一的开销。

如果字符串太长会不会出现堆栈溢出?

在这种情况下没有堆栈溢出。字符串文字通常存储在只读存储器中,而不是堆栈存储器中。当字符串传递给

printf
时,只有指向其第一个元素的指针被复制到堆栈中。

编译器将处理这个多行字符串

"line 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"      
"line 8\n"
"line 9\n.. etc"  

作为单字符串

"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\n.. etc"  

这将存储在内存的只读部分。

但请注意(由评论中的pmg指出)C11标准部分5.2.4.1翻译限制

实现应能够翻译和执行至少一个程序,其中至少包含以下每一项限制的一个实例18):
[...]

  • 字符串文字中有 4095 个字符(连接后)
    [...]

11
投票

C 会连接不以任何内容或空格分隔的字符串文字。那么下面

printf( "line 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"      
"line 8\n"
"line 9\n.. etc"); 

非常好,并且在可读性方面很突出。此外,毫无疑问,单个

printf
调用的开销比 9 个
printf
调用要少。


10
投票
如果您仅输出常量字符串,则

printf
是一个缓慢的函数,因为
printf
必须扫描每个字符以查找格式说明符 (
%
)。像
puts
这样的函数对于长字符串来说明显更快,因为它们基本上可以将输入字符串
memcpy
输入输出 I/O 缓冲区。

许多现代编译器(GCC、Clang,可能还有其他编译器)都有一项优化,如果输入字符串是不带格式说明符且以换行符结尾的常量字符串,则自动将

printf
转换为
puts
。例如,编译以下代码:

printf("line 1\n");
printf("line 2\n");
printf("line 3"); /* no newline */

产生以下程序集(Clang 703.0.31,

cc test.c -O2 -S
):

...
leaq    L_str(%rip), %rdi
callq   _puts
leaq    L_str.3(%rip), %rdi
callq   _puts
leaq    L_.str.2(%rip), %rdi
xorl    %eax, %eax
callq   _printf
...

换句话说,

puts("line 1"); puts("line 2"); printf("line 3");

如果您的长

printf
字符串以换行符结尾,那么您的性能可能会比使用换行符结尾的字符串进行一堆 printf 调用更糟糕,仅仅是因为这种优化。为了进行演示,请考虑以下程序:

#include <stdio.h> #define S "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" #define L S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S /* L is a constant string of 4000 'a's */ int main() { int i; for(i=0; i<1000000; i++) { #ifdef SPLIT printf(L "\n"); printf(S); #else printf(L "\n" S); #endif } }

如果 
SPLIT

未定义(生成单个

printf
且没有终止换行符),则时间如下所示:

[08/11 11:47:23] /tmp$ cc test.c -O2 -o test [08/11 11:47:28] /tmp$ time ./test > /dev/null real 0m2.203s user 0m2.151s sys 0m0.033s

如果定义了
SPLIT

(产生两个

printf
,一个带有终止换行符,另一个没有),则时间如下所示:

[08/11 11:48:05] /tmp$ time ./test > /dev/null real 0m0.470s user 0m0.435s sys 0m0.026s

所以你可以看到,在这种情况下,将 
printf

分成两部分实际上会产生 4 倍的加速。当然,这是一个极端的情况,但它说明了

printf
如何根据输入进行不同的优化。 (请注意,使用
fwrite
甚至更快 - 0.197 秒 - 所以如果你真的想要速度,你应该考虑使用它!)。

tl;dr:如果您只打印大的常量字符串,请完全避免

printf

并使用更快的函数,例如

puts
fwrite
    

不带格式修饰符的

5
投票
会被静默替换(也称为优化)为

puts

 调用。这已经是加速了。您真的不想在多次调用 
printf
/
puts
 时失去它。
GCC 有 
printf

(以及其他)作为内置函数,因此它可以在编译时优化调用。

参见:

https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
  • http://www.ciselant.de/projects/gcc_printf/gcc_printf.html
  • https://gcc.gnu.org/ml/gcc-patches/2000-09/msg00826.html
每个额外的 printf (或者 put,如果你的编译器以这种方式优化它)每次都会产生系统特定的函数调用开销,尽管优化很有可能将它们组合起来。

1
投票
我还没有看到作为叶函数的 printf 实现,因此预计 vfprintf 及其被调用者等函数会产生额外的函数调用开销。

那么每次写入可能会产生某种系统调用开销。由于 printf 使用缓冲的 stdout,因此通常可以避免其中一些(非常昂贵的)上下文切换......除了上面的所有示例都以新行结尾。您的大部分成本可能都在这里。

如果您真的担心主线程中的成本,请将此类内容移至单独的线程中。

© www.soinside.com 2019 - 2024. All rights reserved.