【问题标题】:Why didn't GCC optimize this 'printf' to 'puts'?为什么 GCC 不将此“printf”优化为“puts”?
【发布时间】:2017-06-07 07:58:16
【问题描述】:

这是我的测试代码:

#include<stdio.h>

static inline void foo(int a){
    printf("%x\n", a);  
}

int main(void){
    foo(0x1234);    
    return 0;
}

我认为 GCC 应该意识到 a 是一个字面整数,并优化为这样的代码:

puts("1234");

但我得到了以下汇编代码:

│0x8048341 <main+17>     push   $0x1234                                        
│0x8048346 <main+22>     push   $0x80484e0                                  
│0x804834b <main+27>     push   $0x1                                        
│0x804834d <main+29>     call   0x8048310 <__printf_chk@plt> 

我的项目中有很多这样的代码,因为我一直相信 GCC 会为我优化,甚至在某些可以简单地使用 'write()' 的情况下,我坚持使用printf,因为我认为我会从它的缓冲机制中受益。

现在我感到很遗憾,因为解析格式字符串的开销会扼杀我所拥有的任何收益。我项目中的这些代码非常底层,可能会导致性能瓶颈。

【问题讨论】:

  • 依靠编译器为你神奇地优化事物通常是一种无望的做法。
  • 我不认为编译器曾经可以优化库调用。您可以链接一个库,其中printf() 具有编译器在编译阶段无法知道的副作用。
  • @FelixPalmen 不,编译器可以,因为printf() 是由 C 标准定义的。
  • @Stargateur 实际上,为%x 传递int 可能是明确定义的,因为va_arg 允许混合签名(C99 中的7.15.1.1)。 GCC 和 Clang 似乎同意这种解释,因为即使使用 -Wformat=2,它们也不会发出警告。
  • @M.M GCC 仍然需要根据格式确定要输出的正确字符串。实现这种优化当然是可能的,但也许它被认为不值得维护printf 的副本的麻烦。也许几年后我们会得到它? :)

标签: c gcc assembly compiler-optimization


【解决方案1】:

我项目中的这些代码非常底层,可能会导致性能瓶颈。

首先,我可以打消您的担忧,即这是不可能的。控制台 I/O 的开销是巨大的(相对而言),所以无论你使用什么方法,这始终是你代码中的瓶颈。

我认为 gcc 应该意识到 a 是一个字面整数,并优化为这样的代码:

puts("1234");

显然不是。 GCC(和 Clang)does perform an optimization where printf("...\n"); is transformed into puts("...");,如您所见 here,但这仅在您将 字符串文字printf 一起使用时才会发生。优化器(当前)不查看格式字符串、解析它并围绕它进行优化。你打电话给printf,所以你得到printf

编译器优化不是保证,因此您不应该在没有首先验证所需优化实际上在所有情况下都应用的情况下编写依赖的代码您感兴趣的(包括代码变体、编译器版本、目标平台等)。

如果您希望将此作为 GCC 优化器的改进建议,您可以建议对 their Bugzilla 进行增强。但不要屏息以待它很快就会实施。考虑到可预期的实际性能改进充其量是最小的(见上文),实现此类优化所需的逻辑并不值得付出努力。

与此同时,如果您绝对需要对代码进行最少更改的这种优化,那么您可以使用some macro hackery

#define STRINGIFY_INTERNAL(x)  #x
#define STRINGIFY(x)           STRINGIFY_INTERNAL(x)

#define foo(a)                 puts(STRINGIFY(a))

这确实会产生所需的输出:

.LC0:
        .string "0x1234"
MyFunction:
        sub     esp, 24
        push    OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     esp, 28
        ret

【讨论】:

  • 原来clang会将const char fmt[] = = {'h', 'i', '\n', 0};/printf(fmt);优化成putsgodbolt.org/g/aTQRKX 但对于 gcc,它确实必须是字符串文字。 (至少间接地;const char*fmt = "hi\n"; 然后将其传递给 printf 优化,只要 printf 没有其他参数。)
  • 有趣的观察,@Peter。我没有考虑过这种变化,老实说,我有点惊讶 GCC 的模式匹配优化在那里不起作用。
  • 我也很惊讶,但它是有道理的。我猜它不会将 char 数组或它们的初始化程序视为字符串常量,因为它必须检查它们最后是否只包含一个 0 字节。您可以在 asm 输出中看到这一点,其中 gcc 使用单独的 .byte 指令而不是 .asciz,但 clang 注意到类似字符串的性质并发出 .asciz 以及进行 puts 优化。
  • 嗯,我想知道在 char 数组而不是字符串上是否存在使用 strlen()strchr() 进行的实际错过优化?对于 char 数组,这个 printf/puts 似乎很不自然。
【解决方案2】:

符合标准的库实现可以包含标准定义之外的函数,这将改变标准函数的行为方式。例如,一个库可能包含一个 __select_alternate_digits 函数,当调用该函数时,将导致对 printf 的后续调用以使用非正常数字的方式显示数字。

有了这样的库,给定代码:

#include <stdio.h> // Could legitimately include functions that aren't
                  // defined by the Standard, but which start with __.

int main(void)
{
  __select_alternate_digits("⁰¹²³⁴⁵⁶⁷⁸⁹");
  printf("%d",123);
  __select_alternate_digits(0); // Reset to default set      
}

__select_alternate_digits 的调用可能会导致上述程序输出“¹²³”而不是“123”。如果编译器捆绑了它自己的printf 函数,它就可以知道它的行为不会受到任何其他函数调用的影响。但是,如果它使用外部库,那么除非程序完全没有对编译器一无所知的函数的任何调用,否则高质量的编译器应该假定这些函数可能具有编译器无法预测的影响。

【讨论】:

  • 这并没有真正解释什么:printf("123\n");is replaced by a call to puts,而 OP 的代码不是,尽管原始函数是相同的:printf,所以编译器确实知道关于它。
  • @Ruslan:printf 的实现对不包含% 的字符串进行任何类型的翻译都是不寻常的。它认为 gcc 假设他们不会这样做可能有点冒昧,但它越深入到非平凡的格式说明符领域,麻烦的可能性就越大。但是,我确实忘记了另一点:包含 printf 和非平凡格式字符串的代码的用途之一是 test printf 实现。如果程序员写printf("Hello");,那么程序员可能对输出Hello\n 比... 更感兴趣
  • ...在测试printf 是否可以正确处理此类字符串。但是给定printf("%d\n",-2147483647-1);,程序员可能对输出-2147483648\n 不感兴趣,而是对确认特定printf 实现正确处理INT_MIN 情况感兴趣。让编译器自己进行格式化会破坏这个目标。
  • re:测试 printfmemcpy 或任何实现:使用 gcc -fno-builtin-printf
  • glibc printf 确实允许通过注册新的转换,甚至覆盖标准转换说明符进行自定义。 (gnu.org/software/libc/manual/html_node/Customizing-Printf.html)。所以在 GNU 系统中,只有在格式字符串不包含转换时转换 printf 调用才是安全的,因为在 main 之前运行的东西可能会改变 %x 的行为。
猜你喜欢
  • 2021-11-23
  • 2016-07-20
  • 2016-09-22
  • 2010-09-12
  • 2021-11-22
  • 2016-09-04
  • 2017-09-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多