-O2将printf(“%s \ n”,str)优化为puts(str)

clang玩弄,我编译了一个包含这一行的C程序:

 printf("%s\n", argv[0]); 

在没有优化的情况下进行编译时,在设置寄存器后,程序集输出称为printf

 movq (%rcx), %rsi movq %rax, %rdi movb $0, %al callq _printf 

我尝试使用clang -O2编译。 printf调用被替换为puts调用:

 movq (%rsi), %rdi callq _puts 

虽然这在这种情况下非常有意义,但它提出了两个问题:

  1. 在优化编译中,函数调用替换的频率是多少? 这是频繁的还是stdio是个例外?
  2. 我可以为自己的库编写编译器优化吗? 我该怎么办?

  1. 在优化编译中,函数调用替换的频率是多少? 这是频繁的还是stdio是个例外?

使用LLVM替换printf的优化是在LibCallSimplifier类中。 您可以在llvm / include / llvm / Transforms / Utils / SimplifyLibCalls.h中查看头文件,并在llvm / lib / Transforms / Utils / SimplifyLibCalls.cpp中查看实现。 查看这些文件将给出一些已完成的其他库调用替换优化的示例(头文件可能更容易开始)。 当然还有LLVM所做的其他许多其他优化,你可以通过查看LLVM传递列表来了解其中的一些优化。

  1. 我可以为自己的库编写编译器优化吗? 我该怎么办?

是的,你可以。 LLVM非常模块化,可以在一系列过程中对IR进行转换。 因此,如果您想为自己的库添加一个自定义传递,您可以这样做(尽管理解LLVM编译器流如何工作仍然是一项相当大的工作)。 一个很好的起点是文档: 编写LLVM Pass 。

这种优化取决于编译器知道名为printf只能是C标准定义的printf函数。 如果程序将printf定义为其他内容,则程序将调用未定义的行为。 这使得编译器可以在调用标准printf函数的情况下替换调用printf 。 它并不担心它“工作”就像用户定义的printf函数被调用一样。 因此,这些函数替换优化几乎仅限于C或C ++标准中定义的函数。 (如果编译器以某种方式知道给定的标准生效,也许还有其他标准。)

如果没有自己修改编译器的源代码,就无法通过自己的函数告诉编译器这些函数替换是可行的。 但是,由于存在限制,您可以使用内联函数执行类似操作。 例如,你可以用类似这样的东西实现类似于printf / puts优化的东西:

 inline int myprintf(char const *fmt, char const *arg) { if (strcmp(fmt, "%s\n") == 0) { return myputs(args); } return _myprintf_impl(fmt, arg) } 

启用优化后,编译器可以在编译时选择基于fmt参数调用哪个函数,但只有在它可以确定它是一个常量字符串时。 如果它不能,或者没有启用优化,那么编译器必须发出在每次调用时检查它的代码,这很容易将其转化为悲观。 请注意,此优化取决于编译器知道strcmp如何工作以及完全删除调用,因此编译器可以进行另一个库函数调用替换的示例。

你可以使用GCC的__builtin_constant_p函数改进这个:

 inline int myprintf(char const *fmt, char const *arg) { if (__builtin_constant_p(fmt[0]) && strcmp(fmt, "%s\n") == 0) { return myputs(arg); } return _myprintf_impl(fmt, arg); } 

在GCC下,这导致代码永远不会检查格式字符串的运行时间。 如果在编译时可以确定fmt"%s\n"那么它会生成无条件调用myputs代码,否则它将生成无条件调用_myprintf_impl代码。 因此,启用优化后,此function永远不会是悲观。 不幸的是,当clang支持__builtin_constant_p函数时,我的clang版本总是生成无条件调用_myprintf_impl代码。

puts是一个比printf小得多的函数,可执行文件的大小通常只有一半。 只有在将数字转换为字符串进行打印时才需要printf,您可以使用itoa()执行此操作