-O2 优化 printf("%s\n", str) 到 puts(str)。

5

我在尝试使用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
我不知道(否则我会回答),但据我所知,只有一些特定的标准库函数拥有这种特殊情况。memset是另一个例子,编译器通常通过识别它是否适用于固定小尺寸并将其内联处理来进行特殊处理。 - ShadowRanger
2
只要你遵守C标准,你可以做任何想做的事情。 - M.M
类似 double x = sqrt(2.0) 的代码可能会被优化为 double x = 1.4142135.....。这取决于编译器和设置。 - chux - Reinstate Monica
1
@M.M 我认为他在问 clang 是否提供了库作者可以使用的机制来扩展优化,还是必须通过修改编译器本身来完成。 - Barmar
gcc 和 clang 都将许多函数视为内置函数。例如,即使您根本不包含任何头文件,一些函数(如 memsetmemcpystrlen 等)仍将内联其内置实现,或进行常量传播。例如,int foo(){return strlen("hello world!"); }编译为return 12,即使没有 <string.h> - Peter Cordes
显示剩余3条评论
3个回答

3
在优化编译中,函数调用替换发生的频率有多高?这是经常发生的还是stdio是一个例外?LLVM中替换printf为puts的优化属于LibCallSimplifier类。您可以在llvm/include/llvm/Transforms/Utils/SimplifyLibCalls.h中查看头文件,在llvm/lib/Transforms/Utils/SimplifyLibCalls.cpp中查看实现。查看这些文件将展示一些其他库调用替换优化的示例(从头文件开始可能更容易)。当然,LLVM还有许多其他优化,您可以通过查看LLVM passes列表来了解其中一些。
  1. 我能为自己的库编写编译器优化吗?我该如何做?

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


2
这种优化依赖于编译器知道名为printf的函数只能是C标准定义的printf函数。如果程序将printf定义为其他含义,则程序会引发未定义行为。这使得编译器可以在适用“as if”标准printf函数的情况下替换为调用puts。它不必担心它是否适用于用户定义的printf函数。因此,这些类型的函数替换优化基本上仅限于C或C ++标准中定义的函数。(如果编译器以某种方式知道给定的标准正在生效,则可能还有其他标准。)
除了自己修改编译器源代码外,没有办法告诉编译器这些函数替换可以使用自己的函数。但是,有限制条件下,您可以使用内联函数类似地实现类似于printf/puts优化的功能。例如,您可以使用以下内容实现类似于: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的代码。因此,在启用优化的情况下,此函数永远不是一个性能劣化。不幸的是,尽管clang支持__builtin_constant_p函数,但我的版本的clang总是会生成无条件调用_myprintf_impl的代码。

-4

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


这并没有回答任何一个问题。 - R_Kapp
1
这并没有真正回答问题。楼主似乎非常清楚这是一个合理的优化。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接