【问题标题】:Multiple printf() calls vs one printf() call with a long string?多个 printf() 调用与一个带有长字符串的 printf() 调用?
【发布时间】:2016-08-11 12:59:05
【问题描述】:

假设我有一行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 语法无效。
  • @ammoQ;对我来说似乎有效。
  • 函数调用开销会有所不同(多次调用一个函数)。不太可能发生堆栈溢出......字符串文字具有静态存储持续时间,并且每次调用仅将第一个字符的地址传递给printf()。做任何让你的代码更容易被人类理解的事情。
  • 堆栈溢出不是这里的问题,因为字符串文字是const char 并且字符串的字符被推入“只读内存”部分:静态存储。
  • 注意您的实施限制。 C11 Standard requires implementations to accept string literals of up to 4095 characters。除此之外,您将进入实现定义的领域。

标签: c string printf


【解决方案1】:

与每行有多个 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"  

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

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

实现应能够翻译和执行至少一个程序,该程序包含以下每个限制的至少一个实例18)
[...]
- 字符串文字中的 4095 个字符(连接后)
[...]

【讨论】:

  • 次要注意:“字符串文字存储在只读内存中”--> 可能。字符串文字用于阅读。 “写”给他们的是UB。
【解决方案2】:

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 调用要少。

【讨论】:

    【解决方案3】:

    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(产生两个printfs,一个带有终止换行符,另一个没有),时序如下所示:

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

    所以你可以看到,在这种情况下,将printf 分成两部分实际上会产生 4 倍的加速。当然,这是一个极端情况,但它说明了printf 可以如何根据输入进行可变优化。 (请注意,使用 fwrite 甚至更快 - 0.197s - 所以如果你真的想要速度,你应该考虑使用它!)。

    tl;dr:如果您只打印较大的常量字符串,请完全避免使用 printf,并使用更快的函数,例如 putsfwrite

    【讨论】:

    • OP 应该首先使用printf("%s", "very long string"),这样可以缓解问题,但这是个好建议。
    【解决方案4】:

    没有格式修饰符的printf 被静默替换(又名优化)为puts 调用。这已经是一个加速。你真的不想在多次调用printf/puts 时失去它。

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

    见:

    【讨论】:

    • 你有这方面的资料吗?我想阅读更多内容。谢谢。
    • 我在跟踪二进制文件时发现了这一点,并想知道为什么当我专门打印信息时它会调用 puts。
    • 这不是真的。对于特定的编译器,它可能是正确的。例如,gcc for arm 将用相应的 arm 指令替换所有操作 - 当然,这完全不相关,只是一个实现细节,就像 printf 优化一样。
    • 这是几乎所有编译器中的编译器特定优化。 OP问了这个问题。
    【解决方案5】:

    每个额外的 printf(或 puts,如果您的编译器以这种方式优化它)每次都会产生系统特定的函数调用开销,尽管优化很有可能无论如何都会将它们结合起来。

    我还没有看到作为叶函数的 printf 实现,因此预计 vfprintf 之类的函数调用开销会增加,它是被调用者。

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

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

    【讨论】:

    猜你喜欢
    • 2022-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    • 2022-01-18
    • 1970-01-01
    相关资源
    最近更新 更多