static-keys
Static key是Linux内核中的一个底层机制,用于加速对很少改变的特性的条件判断检查。我们将这一特性迁移到了用户态Rust程序中,适用于Linux、macOS和Windows。(目前需要Nightly版本的Rust,原因请参见FAQ)
目前在CI中经过测试的支持平台包括:
-
Linux
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
i686-unknown-linux-gnu
aarch64-unknown-linux-gnu
riscv64gc-unknown-linux-gnu
loongarch64-unknown-linux-gnu
-
macOS
aarch64-apple-darwin
-
Windows
x86_64-pc-windows-msvc
i686-pc-windows-msvc
-
裸金属(未经过CI测试)
- 上述架构应该都能支持。具体可见FAQ
需要注意,如果使用cross-rs交叉编译loongarch64-unknown-linux-gnu
平台,需要使用GitHub上的最新版cross-rs。更多细节可参见Evian-Zhang/static-keys#4。
更详细的解释和FAQ可参见GitHub Pages(English version).
出发点
现代程序往往可以通过命令行选项或配置文件进行配置。在配置中开启或关闭的选项往往在程序启动后是不会被改变的,但由在整个程序中被频繁访问。
let flag = CommandlineArgs::parse();
loop {
if flag {
do_something();
}
do_common_routines();
}
尽管flag
在程序初始化后不会被修改,在每次循环中仍然会执行if
判断。在x86-64架构中,这一判断一般会被编译为test
-jnz
指令
test eax, eax ; 检查eax寄存器是否为0
jnz do_something ; 如果不为0,则跳转do_something
do_common_routines:
; 执行相关指令
ret
do_something:
; 执行相关指令
jmp do_common_routines ; 跳转至do_common_routines
尽管if
判断只是test
-jnz
指令,这还可以进一步加速。我们是不是可以把这个判断优化为jmp
指令(不执行do_something
分支)或nop
指令(总是执行do_something
分支)?这就是static keys的原理。简单来说,我们需要动态修改指令。在从命令行获得flag
之后,我们根据flag
的值将if flag {}
动态修改为jmp
或nop
指令。
例如,如果用户给出的flag
为false
,那么生成的指令将被动态修改为下面的nop
指令
nop DWORD PTR [rax+rax*1+0x0]
do_common_routines:
; 执行相关指令
ret
do_something:
; 执行相关指令
jmp do_common_routines
如果flag
为true
,那么生成的指令将被修改为下面的无条件跳转指令
jmp do_something
do_common_routines:
; 执行相关指令
ret
do_something:
; 执行相关指令
jmp do_common_routines
这里的指令就不会再有test
和条件跳转指令了,而仅仅是一个nop
(即不做任何事)或jmp
指令。
尽管将test
-jnz
替换为nop
提升的性能可能微乎其微,但是Linux内核文档中描述了,如果这样的配置检查涉及全局变量,那么这样的替换可以减小缓存压力。并且在服务端程序中,这些配置有可能会通过Arc
在多线程中共享,那么用nop
或jmp
就可以有更大的提升空间。
使用方式
目前需要nightly版本的Rust来使用这个库。在使用者的代码中,需要声明对unstable特性asm_goto
的使用。
#![allow(unused)] #![feature(asm_goto)] fn main() { }
首先,在Cargo.toml
中加入相应依赖:
[dependencies]
static-keys = "0.6"
在main
函数开头,需要调用static_keys::global_init
进行初始化。
fn main() { static_keys::global_init(); // 执行其他指令 }
随后需要定义一个static key来记录用户传入的flag,并根据用户传入的值来控制这个static key的值。
#![allow(unused)] fn main() { use static_keys::define_static_key_false; struct CommandlineArgs {} impl CommandlineArgs { fn parse() -> bool { true } } // FLAG_STATIC_KEY初始值为`false` define_static_key_false!(FLAG_STATIC_KEY); fn application_initialize() { let flag = CommandlineArgs::parse(); if flag { unsafe { FLAG_STATIC_KEY.enable(); } } } }
同一个static key可以在任意时刻多次更改值。但需要注意的是,在多线程环境中修改static key是非常危险的。因此,如果需要使用多线程,请在完成对static key的修改后再创建新线程。不过,在多线程环境中使用static key是绝对安全的。此外,对static key的修改相对比较慢,但由于static key一般用于控制很少被修改的特性,所以这样的修改相对比较少,因此慢点也没有太大影响。请参见FAQ了解更多。
在定义static key之后,就可以像平常一样用if
语句来使用这个static key了(这个和这个介绍了likely
和unlikely
API的语义)。同一个static key可以在多个if
语句中被使用。当这个static key被修改时,所有使用这个static key的if
语句都将被统一修改为jmp
或nop
。
#![allow(unused)] fn main() { #![feature(asm_goto)] use static_keys::{define_static_key_false, static_branch_unlikely}; struct CommandlineArgs {} impl CommandlineArgs { fn parse() -> bool { true } } fn do_something() {} fn do_common_routines() {} define_static_key_false!(FLAG_STATIC_KEY); fn run() { loop { if static_branch_unlikely!(FLAG_STATIC_KEY) { do_something(); } do_common_routines(); } } }
参考链接
- Linux内核官方文档:static-keys
- Linux
static_key
internals - Rust for Linux项目也实现了static key,请查看Rust-for-Linux/linux#1084。我们基于
break
的内联汇编实现是受到这一实现的启发的。