我正在为嵌入式平台编写一个C库,该平台的固件有几个用于字符输出的例程。其中一个子例程是通用的 - 将要打印的 ASCII 字符加载到寄存器中,调用子例程,然后将字符打印出来。然而,其他一些子例程会跳过寄存器加载并输出特定的控制字符,例如响铃字符和回车符。
在我的
putchar()
实现中(printf
使用),我可能想将这些例程中的每一个用于不同的字符:
void putchar(char c) {
if (__builtin_expect(c == '\n', 0)) call_line_feed_out();
else if (__builtin_expect(c == '\a', 0)) call_bell_out();
else call_cout(c);
}
在编译
printf
用于通用输入时,我希望 LLVM 将 putchar()
直接优化为 call_cout
并跳过分支。但是,如果 LLVM 能够将所有内容编译为 putchar
调用(例如足够小的常量字符串打印),我希望 LLVM 使用这些特殊端点来节省内存。
我想如果 LLVM 知道的话,它自然会这样做,比如
call_line_feed_out()
和 call_cout('\n')
做了同样的事情,并且它会为其中一个选择最佳的代码路径。这是我可以做的事情,还是我需要进一步执行这种区分(例如让 printf
直接调用 call_cout
)?
(请注意,这些
call_
函数的主体是进行固件调用的 asm volatile
语句,LLVM 无权访问这些调用的列表。不过,如果这是一个选项,我确实可以在手并且可以以某种方式将这些提供给LLVM,这样它就可以自己做出决定。)
我不知道有什么方法可以告诉编译器它可以进行不同的调用,但在这种情况下我们可以用不同的方式实现相同的目标。
仅对编译时常量进行特殊处理的常用方法是使用
__builtin_constant_p()
在内联 + 常量折叠之后测试变量的值在编译时是否已知。 (较早的 Clang 过去常常在内联之前过早地评估 __builtin_constant_p()
,因此函数参数永远不会是常量,这使得它除了在宏中之外有些无用。但这个问题后来被修复了,使其像 GCC 预期的那样工作。)
GCC 内置函数的 _p
命名来自 Lisp。这是一个“谓词”函数 - 它询问是/否问题并返回一个布尔值。
void my_putchar(char c) {
if (__builtin_constant_p(c)) {
// only reached if c is a compile-time constant after inlining
if (c == '\n') { call_line_feed_out(); return; }
else if (c == '\a') { call_bell_out(); return; }
}
// else runtime variable or a constant that wasn't one of those special cases
call_cout(c);
}
my_putchar
的非内联定义只是对
call_cout
的尾部调用。 (如果我们能以某种方式使该符号成为 call_cout
的别名,那就更好了,这样执行就不必通过该单指令函数来回跳。)或者,当内联非常量或使用 call_cout
或
'\n'
以外的字符时,这只是对 '\a'
的常规调用。void use_putchar_newline(void){
my_putchar('\n');
}
编译为
b call_line_feed_out
- 尾调用。
void use_putchar_test(char *str){
my_putchar(str[0]);
my_putchar(str[1]);
my_putchar(str[2]);
const char *strconst = "ab\n";
my_putchar(strconst[0]);
my_putchar(strconst[1]);
my_putchar(strconst[2]);
}
为 AArch64 编译,带有clang on Godbolt
-O3 -fomit-frame-pointer
。 (我以为 Linux 的
-fomit-frame-pointer
默认会在 -O3
处打开,但显然并非如此。)use_putchar_test:
stp x30, x19, [sp, #-16]! // 16-byte Folded Spill // save return address and a call-preserved reg
mov x19, x0 // copy str to a call-preserved reg
ldrb w0, [x0] // my_putchar(str[0]);
bl call_cout
ldrb w0, [x19, #1]
bl call_cout
ldrb w0, [x19, #2]
bl call_cout // my_putchar(str[2]);
mov w0, #97
bl call_cout // my_putchar(strconst[0])
mov w0, #98
bl call_cout
ldp x30, x19, [sp], #16 // 16-byte Folded Reload
b call_line_feed_out // my_putchar(strconst[2]) tailcall
因此,对于未知的运行时变量输入,我们没有运行时分支,并且我们仍然可以从字符串文字中分派到常量换行符的特殊情况函数。
GCC 编译它大致相同。