Static Keys¶
警告
已废弃的 API (DEPRECATED API)
直接使用 ‘struct static_key’ 现已废弃。此外,static_key_{true,false}() 也已废弃。即:请勿使用以下内容
struct static_key false = STATIC_KEY_INIT_FALSE;
struct static_key true = STATIC_KEY_INIT_TRUE;
static_key_true()
static_key_false()
更新后的替代 API 为
DEFINE_STATIC_KEY_TRUE(key);
DEFINE_STATIC_KEY_FALSE(key);
DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
static_branch_likely()
static_branch_unlikely()
摘要¶
Static keys 允许通过 GCC 特性和代码修补(code patching)技术,在性能敏感的内核快速路径(fast-path)代码中包含极少使用的功能。一个简单的示例
DEFINE_STATIC_KEY_FALSE(key);
...
if (static_branch_unlikely(&key))
do unlikely code
else
do likely code
...
static_branch_enable(&key);
...
static_branch_disable(&key);
...
static_branch_unlikely() 分支将被生成到代码中,对可能执行的代码路径(likely code path)产生尽可能小的影响。
动机¶
目前,跟踪点(tracepoints)是使用条件分支实现的。条件检查需要为每个跟踪点检查一个全局变量。尽管此检查的开销很小,但当内存缓存承受压力时,开销会增加(这些全局变量的内存缓存行可能会与其他内存访问共享)。随着我们增加内核中跟踪点的数量,这种开销可能会成为更多的问题。此外,跟踪点通常处于休眠状态(禁用)且不提供直接的内核功能。因此,非常希望尽可能减少其影响。尽管跟踪点是这项工作的最初动机,但其他内核代码路径也应该能够利用 static keys 机制。
解决方案¶
gcc (v4.5) 增加了一条新的 ‘asm goto’ 语句,允许跳转到标签
https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html
使用 ‘asm goto’,我们可以创建默认执行或默认不执行的分支,而无需检查内存。然后,在运行时,我们可以修补分支点以更改分支方向。
例如,如果我们有一个默认禁用的简单分支
if (static_branch_unlikely(&key))
printk("I am the true branch\n");
因此,默认情况下不会发出 ‘printk’。生成的代码将由顺序代码路径中的单个原子 ‘no-op’ 指令(在 x86 上为 5 个字节)组成。当分支被“翻转(flipped)”时,我们将用一条跳转到行外(out-of-line)true 分支的 ‘jump’ 指令来修补顺序代码路径中的 ‘no-op’。因此,更改分支方向开销很大,但分支选择基本是“免费”的。这就是该优化的基本权衡。
这种底层修补机制称为“跳转标签修补(jump label patching)”,它为 static keys 机制提供了基础。
Static key 标签 API、用法和示例¶
为了利用此优化,你必须首先定义一个 key
DEFINE_STATIC_KEY_TRUE(key);
或者
DEFINE_STATIC_KEY_FALSE(key);
该 key 必须是全局的,也就是说,它不能分配在栈上,也不能在运行时动态分配。
然后,该 key 在代码中这样使用:
if (static_branch_unlikely(&key))
do unlikely code
else
do likely code
或者
if (static_branch_likely(&key))
do likely code
else
do unlikely code
通过 DEFINE_STATIC_KEY_TRUE() 或 DEFINE_STATIC_KEY_FALSE 定义的 key,可以在 static_branch_likely() 或 static_branch_unlikely() 语句中使用。
可以通过以下方式将分支设为真(true):
static_branch_enable(&key);
或通过以下方式设为假(false):
static_branch_disable(&key);
然后可以通过引用计数来切换分支:
static_branch_inc(&key);
...
static_branch_dec(&key);
因此,带适当引用计数的 ‘static_branch_inc()’ 意味着“使分支为真”,而 ‘static_branch_dec()’ 意味着“使分支为假”。例如,如果 key 初始化为真,则 static_branch_dec() 会将分支切换为假。随后的一次 static_branch_inc() 会将分支改回真。同样,如果 key 初始化为假,‘static_branch_inc()’ 会将分支更改为真。然后 ‘static_branch_dec()’ 会再次使分支变为假。
可以使用 ‘static_key_enabled()’ 和 ‘static_key_count()’ 来获取状态和引用计数。通常,如果你使用这些函数,它们应该受到用于包围启用/禁用或增加/减少函数的同一个互斥锁(mutex)的保护。
请注意,切换分支会导致获取某些锁,特别是 CPU 热插拔锁(以避免在内核被修补时与正在被引入内核的 CPU 产生竞态)。因此,在热插拔通知程序(hotplug notifier)内部调用 static key API 必定会导致死锁。为了仍然允许使用该功能,提供了以下函数:
static_key_enable_cpuslocked()static_key_disable_cpuslocked()static_branch_enable_cpuslocked()static_branch_disable_cpuslocked()
这些函数不是通用的,并且只有在你非常清楚自己处于上述上下文而非其他上下文时才能使用。
如果需要 key 数组,可以将其定义为
DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
或者
DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
架构级代码修补接口,“跳转标签(jump labels)”
为了利用此优化,架构必须实现一些函数和宏。如果没有架构支持,我们只需退回到传统的加载、测试和跳转序列。此外,struct jump_entry 表必须至少按 4 字节对齐,因为 static_key->entry 字段利用了最低的两个有效位。
select HAVE_ARCH_JUMP_LABEL,参见:arch/x86/Kconfig
#define JUMP_LABEL_NOP_SIZE,参见:arch/x86/include/asm/jump_label.h
__always_inline bool arch_static_branch(struct static_key *key, bool branch),参见:arch/x86/include/asm/jump_label.h
__always_inline bool arch_static_branch_jump(struct static_key *key, bool branch),参见:arch/x86/include/asm/jump_label.h
void arch_jump_label_transform(struct jump_entry *entry, enum jump_label_type type),参见:arch/x86/kernel/jump_label.c
struct jump_entry,参见:arch/x86/include/asm/jump_label.h
Static keys / 跳转标签分析,结果 (x86_64)
作为示例,让我们将以下分支添加到 ‘getppid()’ 中,以便系统调用现在看起来像
SYSCALL_DEFINE0(getppid)
{
int pid;
+ if (static_branch_unlikely(&key))
+ printk("I am the true branch\n");
rcu_read_lock();
pid = task_tgid_vnr(rcu_dereference(current->real_parent));
rcu_read_unlock();
return pid;
}
GCC 生成带有跳转标签的结果指令为
ffffffff81044290 <sys_getppid>:
ffffffff81044290: 55 push %rbp
ffffffff81044291: 48 89 e5 mov %rsp,%rbp
ffffffff81044294: e9 00 00 00 00 jmpq ffffffff81044299 <sys_getppid+0x9>
ffffffff81044299: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax
ffffffff810442a0: 00 00
ffffffff810442a2: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax
ffffffff810442a9: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax
ffffffff810442b0: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi
ffffffff810442b7: e8 f4 d9 00 00 callq ffffffff81051cb0 <pid_vnr>
ffffffff810442bc: 5d pop %rbp
ffffffff810442bd: 48 98 cltq
ffffffff810442bf: c3 retq
ffffffff810442c0: 48 c7 c7 e3 54 98 81 mov $0xffffffff819854e3,%rdi
ffffffff810442c7: 31 c0 xor %eax,%eax
ffffffff810442c9: e8 71 13 6d 00 callq ffffffff8171563f <printk>
ffffffff810442ce: eb c9 jmp ffffffff81044299 <sys_getppid+0x9>
没有跳转标签优化的结果看起来像
ffffffff810441f0 <sys_getppid>:
ffffffff810441f0: 8b 05 8a 52 d8 00 mov 0xd8528a(%rip),%eax # ffffffff81dc9480 <key>
ffffffff810441f6: 55 push %rbp
ffffffff810441f7: 48 89 e5 mov %rsp,%rbp
ffffffff810441fa: 85 c0 test %eax,%eax
ffffffff810441fc: 75 27 jne ffffffff81044225 <sys_getppid+0x35>
ffffffff810441fe: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax
ffffffff81044205: 00 00
ffffffff81044207: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax
ffffffff8104420e: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax
ffffffff81044215: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi
ffffffff8104421c: e8 2f da 00 00 callq ffffffff81051c50 <pid_vnr>
ffffffff81044221: 5d pop %rbp
ffffffff81044222: 48 98 cltq
ffffffff81044224: c3 retq
ffffffff81044225: 48 c7 c7 13 53 98 81 mov $0xffffffff81985313,%rdi
ffffffff8104422c: 31 c0 xor %eax,%eax
ffffffff8104422e: e8 60 0f 6d 00 callq ffffffff81715193 <printk>
ffffffff81044233: eb c9 jmp ffffffff810441fe <sys_getppid+0xe>
ffffffff81044235: 66 66 2e 0f 1f 84 00 data32 nopw %cs:0x0(%rax,%rax,1)
ffffffff8104423c: 00 00 00 00
因此,与跳转标签情况(只有一个 ‘no-op’ 或 ‘jmp 0’)相比,禁用了跳转标签的情况增加了 ‘mov’、‘test’ 和 ‘jne’ 指令。(jmp 0 在引导时被修补为 5 字节的原子 no-op 指令。)因此,禁用了跳转标签的情况增加了
6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 addition bytes.
如果我们随后计入填充字节,对于这个小函数,跳转标签代码总共节省了 16 字节的指令内存。在这种情况下,非跳转标签函数长度为 80 字节。因此,我们节省了 20% 的指令占用空间。事实上,我们还可以进一步改进这一点,因为 5 字节的 no-op 实际上可以是一个 2 字节的 no-op,因为我们可以用一个 2 字节的 jmp 到达该分支。然而,我们尚未实现最优的 no-op 大小(目前它们是硬编码的)。
由于调度器路径中多次使用了 static key API,因此可以使用 ‘pipe-test’(也称为 ‘perf bench sched pipe’)来展示性能提升。在 3.3.0-rc2 上进行测试
跳转标签禁用
Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):
855.700314 task-clock # 0.534 CPUs utilized ( +- 0.11% )
200,003 context-switches # 0.234 M/sec ( +- 0.00% )
0 CPU-migrations # 0.000 M/sec ( +- 39.58% )
487 page-faults # 0.001 M/sec ( +- 0.02% )
1,474,374,262 cycles # 1.723 GHz ( +- 0.17% )
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
1,178,049,567 instructions # 0.80 insns per cycle ( +- 0.06% )
208,368,926 branches # 243.507 M/sec ( +- 0.06% )
5,569,188 branch-misses # 2.67% of all branches ( +- 0.54% )
1.601607384 seconds time elapsed ( +- 0.07% )
跳转标签启用
Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):
841.043185 task-clock # 0.533 CPUs utilized ( +- 0.12% )
200,004 context-switches # 0.238 M/sec ( +- 0.00% )
0 CPU-migrations # 0.000 M/sec ( +- 40.87% )
487 page-faults # 0.001 M/sec ( +- 0.05% )
1,432,559,428 cycles # 1.703 GHz ( +- 0.18% )
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
1,175,363,994 instructions # 0.82 insns per cycle ( +- 0.04% )
206,859,359 branches # 245.956 M/sec ( +- 0.04% )
4,884,119 branch-misses # 2.36% of all branches ( +- 0.85% )
1.579384366 seconds time elapsed
节省的分支百分比为 0.7%,我们在 ‘branch-misses’(分支未命中)上节省了 12%。正如我们所预料的,这是获得最多节省的地方,因为此优化的核心就是减少分支数量。此外,我们在指令上节省了 0.2%,在周期数上节省了 2.8%,在耗时上节省了 1.4%。