靜態鍵¶
警告
已棄用的 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()
摘要¶
靜態鍵允許透過 GCC 功能和程式碼修補技術在對效能敏感的快速路徑核心程式碼中包含很少使用的功能。 一個簡單的例子
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() 分支將被生成到程式碼中,儘可能減少對可能程式碼路徑的影響。
動機¶
目前,tracepoint 是使用條件分支實現的。條件檢查需要為每個 tracepoint 檢查一個全域性變數。雖然這種檢查的開銷很小,但當記憶體快取承受壓力時,它會增加(這些全域性變數的記憶體快取行可能與其他記憶體訪問共享)。隨著我們增加核心中 tracepoint 的數量,這種開銷可能會變得更加嚴重。此外,tracepoint 通常處於休眠狀態(停用),不提供直接的核心功能。因此,非常希望儘可能減少它們的影響。雖然 tracepoint 是這項工作的最初動機,但其他核心程式碼路徑應該能夠利用靜態鍵工具。
解決方案¶
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 個位元組),位於直線程式碼路徑中。 當分支被“翻轉”時,我們將用“jump”指令修補直線程式碼路徑中的“no-op”,跳到線外的 true 分支。因此,改變分支方向代價高昂,但分支選擇基本上是“免費的”。 這就是這種最佳化的基本權衡。
這種低階修補機制稱為“跳轉標籤修補”,它為靜態鍵工具提供了基礎。
靜態鍵標籤 API、用法和示例¶
為了利用這種最佳化,您必須首先定義一個鍵
DEFINE_STATIC_KEY_TRUE(key);
或
DEFINE_STATIC_KEY_FALSE(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 定義的鍵可以用於 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()”表示“使分支為假”,並具有適當的引用計數。 例如,如果鍵初始化為 true,則 static_branch_dec() 會將分支切換為 false。 隨後的 static_branch_inc() 會將分支切換回 true。 同樣,如果鍵初始化為 false,“static_branch_inc()”會將分支更改為 true。 然後“static_branch_dec()”將再次使分支為假。
狀態和引用計數可以使用 'static_key_enabled()' 和 'static_key_count()' 來檢索。 一般來說,如果你使用這些函式,它們應該受到與啟用/停用或遞增/遞減函式相同的互斥鎖的保護。
請注意,切換分支會導致鎖定,特別是 CPU 熱插拔鎖(為了避免與將 CPU 引入核心同時核心正在被打補丁的競爭)。 因此,從熱插拔通知器中呼叫靜態鍵 API 肯定會導致死鎖。 為了仍然允許使用該功能,提供了以下函式
static_key_enable_cpuslocked() static_key_disable_cpuslocked() static_branch_enable_cpuslocked() static_branch_disable_cpuslocked()
這些函式不是通用的,只能在您真正知道自己處於上述上下文且沒有其他上下文時使用。
如果需要一個鍵陣列,可以定義為
DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
或
DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
架構級別程式碼修補介面,“跳轉標籤”
架構必須實現一些函式和宏才能利用這種最佳化。 如果沒有架構支援,我們只會退回到傳統的載入、測試和跳轉序列。 此外,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
靜態鍵/跳轉標籤分析,結果 (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
因此,停用跳轉標籤的情況增加了 'mov'、'test' 和 'jne' 指令,而跳轉標籤情況只有一個 'no-op' 或 'jmp 0'。 (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 大小(它們目前是硬編碼的)。
由於排程程式路徑中存在大量靜態鍵 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
已儲存分支的百分比為 .7%,並且我們在“branch-misses”上節省了 12%。 這就是我們期望獲得最大節省的地方,因為這種最佳化是為了減少分支的數量。 此外,我們在指令上節省了 .2%,在週期上節省了 2.8%,在經過的時間上節省了 1.4%。