static-keys

Rust CI status Crates.io Version docs.rs

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

需要注意,如果使用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 {}动态修改为jmpnop指令。

例如,如果用户给出的flagfalse,那么生成的指令将被动态修改为下面的nop指令

    nop     DWORD PTR [rax+rax*1+0x0]
do_common_routines:
    ; 执行相关指令
    ret
do_something:
    ; 执行相关指令
    jmp     do_common_routines

如果flagtrue,那么生成的指令将被修改为下面的无条件跳转指令

    jmp     do_something
do_common_routines:
    ; 执行相关指令
    ret
do_something:
    ; 执行相关指令
    jmp     do_common_routines

这里的指令就不会再有test和条件跳转指令了,而仅仅是一个nop(即不做任何事)或jmp指令。

尽管将test-jnz替换为nop提升的性能可能微乎其微,但是Linux内核文档中描述了,如果这样的配置检查涉及全局变量,那么这样的替换可以减小缓存压力。并且在服务端程序中,这些配置有可能会通过Arc在多线程中共享,那么用nopjmp就可以有更大的提升空间。

使用方式

目前需要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了(这个这个介绍了likelyunlikely API的语义)。同一个static key可以在多个if语句中被使用。当这个static key被修改时,所有使用这个static key的if语句都将被统一修改为jmpnop

#![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();
    }
}
}

参考链接