← Documents Documentation/staging/static-keys.rst GitHub 원문 ↗

Linux 6.18.37 · Staging

Static key와 jump label

폐기된 static_key API의 대체 방법, asm goto 기반 jump-label patching, 상태·reference count·CPU hotplug 규칙, architecture interface와 x86_64 실측을 정리합니다.

Source pathDocumentation/staging/static-keys.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약·해설

static-keys.rst:1-328

폐기된 static_key API의 대체 방법, asm goto 기반 jump-label patching, 상태·reference count·CPU hotplug 규칙, architecture interface와 x86_64 실측을 정리합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 ===========
2 Static Keys
3 ===========
4
5 .. warning::
6
7 DEPRECATED API:
8
9 The use of 'struct static_key' directly, is now DEPRECATED. In addition
10 static_key_{true,false}() is also DEPRECATED. IE DO NOT use the following::
11
12 struct static_key false = STATIC_KEY_INIT_FALSE;
13 struct static_key true = STATIC_KEY_INIT_TRUE;
14 static_key_true()
15 static_key_false()
16
17 The updated API replacements are::
18
19 DEFINE_STATIC_KEY_TRUE(key);
20 DEFINE_STATIC_KEY_FALSE(key);
21 DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
22 DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
23 static_branch_likely()
24 static_branch_unlikely()
25
26 Abstract
27 ========
28
29 Static keys allows the inclusion of seldom used features in
30 performance-sensitive fast-path kernel code, via a GCC feature and a code
31 patching technique. A quick example::
32
33 DEFINE_STATIC_KEY_FALSE(key);
34
35 ...
36
37 if (static_branch_unlikely(&key))
38 do unlikely code
39 else
40 do likely code
41
42 ...
43 static_branch_enable(&key);
44 ...
45 static_branch_disable(&key);
46 ...
47
48 The static_branch_unlikely() branch will be generated into the code with as little
49 impact to the likely code path as possible.
50
51
52 Motivation
53 ==========
54
55
56 Currently, tracepoints are implemented using a conditional branch. The
57 conditional check requires checking a global variable for each tracepoint.
58 Although the overhead of this check is small, it increases when the memory
59 cache comes under pressure (memory cache lines for these global variables may
60 be shared with other memory accesses). As we increase the number of tracepoints
61 in the kernel this overhead may become more of an issue. In addition,
62 tracepoints are often dormant (disabled) and provide no direct kernel
63 functionality. Thus, it is highly desirable to reduce their impact as much as
64 possible. Although tracepoints are the original motivation for this work, other
65 kernel code paths should be able to make use of the static keys facility.
66
67
68 Solution
69 ========
70
71
72 gcc (v4.5) adds a new 'asm goto' statement that allows branching to a label:
73
74 https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html
75
76 Using the 'asm goto', we can create branches that are either taken or not taken
77 by default, without the need to check memory. Then, at run-time, we can patch
78 the branch site to change the branch direction.
79
80 For example, if we have a simple branch that is disabled by default::
81
82 if (static_branch_unlikely(&key))
83 printk("I am the true branch\n");
84
85 Thus, by default the 'printk' will not be emitted. And the code generated will
86 consist of a single atomic 'no-op' instruction (5 bytes on x86), in the
87 straight-line code path. When the branch is 'flipped', we will patch the
88 'no-op' in the straight-line codepath with a 'jump' instruction to the
89 out-of-line true branch. Thus, changing branch direction is expensive but
90 branch selection is basically 'free'. That is the basic tradeoff of this
91 optimization.
92
93 This lowlevel patching mechanism is called 'jump label patching', and it gives
94 the basis for the static keys facility.
95
96 Static key label API, usage and examples
97 ========================================
98
99
100 In order to make use of this optimization you must first define a key::
101
102 DEFINE_STATIC_KEY_TRUE(key);
103
104 or::
105
106 DEFINE_STATIC_KEY_FALSE(key);
107
108
109 The key must be global, that is, it can't be allocated on the stack or dynamically
110 allocated at run-time.
111
112 The key is then used in code as::
113
114 if (static_branch_unlikely(&key))
115 do unlikely code
116 else
117 do likely code
118
119 Or::
120
121 if (static_branch_likely(&key))
122 do likely code
123 else
124 do unlikely code
125
126 Keys defined via DEFINE_STATIC_KEY_TRUE(), or DEFINE_STATIC_KEY_FALSE, may
127 be used in either static_branch_likely() or static_branch_unlikely()
128 statements.
129
130 Branch(es) can be set true via::
131
132 static_branch_enable(&key);
133
134 or false via::
135
136 static_branch_disable(&key);
137
138 The branch(es) can then be switched via reference counts::
139
140 static_branch_inc(&key);
141 ...
142 static_branch_dec(&key);
143
144 Thus, 'static_branch_inc()' means 'make the branch true', and
145 'static_branch_dec()' means 'make the branch false' with appropriate
146 reference counting. For example, if the key is initialized true, a
147 static_branch_dec(), will switch the branch to false. And a subsequent
148 static_branch_inc(), will change the branch back to true. Likewise, if the
149 key is initialized false, a 'static_branch_inc()', will change the branch to
150 true. And then a 'static_branch_dec()', will again make the branch false.
151
152 The state and the reference count can be retrieved with 'static_key_enabled()'
153 and 'static_key_count()'. In general, if you use these functions, they
154 should be protected with the same mutex used around the enable/disable
155 or increment/decrement function.
156
157 Note that switching branches results in some locks being taken,
158 particularly the CPU hotplug lock (in order to avoid races against
159 CPUs being brought in the kernel while the kernel is getting
160 patched). Calling the static key API from within a hotplug notifier is
161 thus a sure deadlock recipe. In order to still allow use of the
162 functionality, the following functions are provided:
163
164 static_key_enable_cpuslocked()
165 static_key_disable_cpuslocked()
166 static_branch_enable_cpuslocked()
167 static_branch_disable_cpuslocked()
168
169 These functions are *not* general purpose, and must only be used when
170 you really know that you're in the above context, and no other.
171
172 Where an array of keys is required, it can be defined as::
173
174 DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
175
176 or::
177
178 DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
179
180 4) Architecture level code patching interface, 'jump labels'
181
182
183 There are a few functions and macros that architectures must implement in order
184 to take advantage of this optimization. If there is no architecture support, we
185 simply fall back to a traditional, load, test, and jump sequence. Also, the
186 struct jump_entry table must be at least 4-byte aligned because the
187 static_key->entry field makes use of the two least significant bits.
188
189 * ``select HAVE_ARCH_JUMP_LABEL``,
190 see: arch/x86/Kconfig
191
192 * ``#define JUMP_LABEL_NOP_SIZE``,
193 see: arch/x86/include/asm/jump_label.h
194
195 * ``__always_inline bool arch_static_branch(struct static_key *key, bool branch)``,
196 see: arch/x86/include/asm/jump_label.h
197
198 * ``__always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)``,
199 see: arch/x86/include/asm/jump_label.h
200
201 * ``void arch_jump_label_transform(struct jump_entry *entry, enum jump_label_type type)``,
202 see: arch/x86/kernel/jump_label.c
203
204 * ``struct jump_entry``,
205 see: arch/x86/include/asm/jump_label.h
206
207
208 5) Static keys / jump label analysis, results (x86_64):
209
210
211 As an example, let's add the following branch to 'getppid()', such that the
212 system call now looks like::
213
214 SYSCALL_DEFINE0(getppid)
215 {
216 int pid;
217
218 + if (static_branch_unlikely(&key))
219 + printk("I am the true branch\n");
220
221 rcu_read_lock();
222 pid = task_tgid_vnr(rcu_dereference(current->real_parent));
223 rcu_read_unlock();
224
225 return pid;
226 }
227
228 The resulting instructions with jump labels generated by GCC is::
229
230 ffffffff81044290 <sys_getppid>:
231 ffffffff81044290: 55 push %rbp
232 ffffffff81044291: 48 89 e5 mov %rsp,%rbp
233 ffffffff81044294: e9 00 00 00 00 jmpq ffffffff81044299 <sys_getppid+0x9>
234 ffffffff81044299: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax
235 ffffffff810442a0: 00 00
236 ffffffff810442a2: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax
237 ffffffff810442a9: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax
238 ffffffff810442b0: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi
239 ffffffff810442b7: e8 f4 d9 00 00 callq ffffffff81051cb0 <pid_vnr>
240 ffffffff810442bc: 5d pop %rbp
241 ffffffff810442bd: 48 98 cltq
242 ffffffff810442bf: c3 retq
243 ffffffff810442c0: 48 c7 c7 e3 54 98 81 mov $0xffffffff819854e3,%rdi
244 ffffffff810442c7: 31 c0 xor %eax,%eax
245 ffffffff810442c9: e8 71 13 6d 00 callq ffffffff8171563f <printk>
246 ffffffff810442ce: eb c9 jmp ffffffff81044299 <sys_getppid+0x9>
247
248 Without the jump label optimization it looks like::
249
250 ffffffff810441f0 <sys_getppid>:
251 ffffffff810441f0: 8b 05 8a 52 d8 00 mov 0xd8528a(%rip),%eax # ffffffff81dc9480 <key>
252 ffffffff810441f6: 55 push %rbp
253 ffffffff810441f7: 48 89 e5 mov %rsp,%rbp
254 ffffffff810441fa: 85 c0 test %eax,%eax
255 ffffffff810441fc: 75 27 jne ffffffff81044225 <sys_getppid+0x35>
256 ffffffff810441fe: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax
257 ffffffff81044205: 00 00
258 ffffffff81044207: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax
259 ffffffff8104420e: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax
260 ffffffff81044215: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi
261 ffffffff8104421c: e8 2f da 00 00 callq ffffffff81051c50 <pid_vnr>
262 ffffffff81044221: 5d pop %rbp
263 ffffffff81044222: 48 98 cltq
264 ffffffff81044224: c3 retq
265 ffffffff81044225: 48 c7 c7 13 53 98 81 mov $0xffffffff81985313,%rdi
266 ffffffff8104422c: 31 c0 xor %eax,%eax
267 ffffffff8104422e: e8 60 0f 6d 00 callq ffffffff81715193 <printk>
268 ffffffff81044233: eb c9 jmp ffffffff810441fe <sys_getppid+0xe>
269 ffffffff81044235: 66 66 2e 0f 1f 84 00 data32 nopw %cs:0x0(%rax,%rax,1)
270 ffffffff8104423c: 00 00 00 00
271
272 Thus, the disable jump label case adds a 'mov', 'test' and 'jne' instruction
273 vs. the jump label case just has a 'no-op' or 'jmp 0'. (The jmp 0, is patched
274 to a 5 byte atomic no-op instruction at boot-time.) Thus, the disabled jump
275 label case adds::
276
277 6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 addition bytes.
278
279 If we then include the padding bytes, the jump label code saves, 16 total bytes
280 of instruction memory for this small function. In this case the non-jump label
281 function is 80 bytes long. Thus, we have saved 20% of the instruction
282 footprint. We can in fact improve this even further, since the 5-byte no-op
283 really can be a 2-byte no-op since we can reach the branch with a 2-byte jmp.
284 However, we have not yet implemented optimal no-op sizes (they are currently
285 hard-coded).
286
287 Since there are a number of static key API uses in the scheduler paths,
288 'pipe-test' (also known as 'perf bench sched pipe') can be used to show the
289 performance improvement. Testing done on 3.3.0-rc2:
290
291 jump label disabled::
292
293 Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):
294
295 855.700314 task-clock # 0.534 CPUs utilized ( +- 0.11% )
296 200,003 context-switches # 0.234 M/sec ( +- 0.00% )
297 0 CPU-migrations # 0.000 M/sec ( +- 39.58% )
298 487 page-faults # 0.001 M/sec ( +- 0.02% )
299 1,474,374,262 cycles # 1.723 GHz ( +- 0.17% )
300 <not supported> stalled-cycles-frontend
301 <not supported> stalled-cycles-backend
302 1,178,049,567 instructions # 0.80 insns per cycle ( +- 0.06% )
303 208,368,926 branches # 243.507 M/sec ( +- 0.06% )
304 5,569,188 branch-misses # 2.67% of all branches ( +- 0.54% )
305
306 1.601607384 seconds time elapsed ( +- 0.07% )
307
308 jump label enabled::
309
310 Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):
311
312 841.043185 task-clock # 0.533 CPUs utilized ( +- 0.12% )
313 200,004 context-switches # 0.238 M/sec ( +- 0.00% )
314 0 CPU-migrations # 0.000 M/sec ( +- 40.87% )
315 487 page-faults # 0.001 M/sec ( +- 0.05% )
316 1,432,559,428 cycles # 1.703 GHz ( +- 0.18% )
317 <not supported> stalled-cycles-frontend
318 <not supported> stalled-cycles-backend
319 1,175,363,994 instructions # 0.82 insns per cycle ( +- 0.04% )
320 206,859,359 branches # 245.956 M/sec ( +- 0.04% )
321 4,884,119 branch-misses # 2.36% of all branches ( +- 0.85% )
322
323 1.579384366 seconds time elapsed
324
325 The percentage of saved branches is .7%, and we've saved 12% on
326 'branch-misses'. This is where we would expect to get the most savings, since
327 this optimization is about reducing the number of branches. In addition, we've
328 saved .2% on instructions, and 2.8% on cycles and 1.4% on elapsed time.
329

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

폐기된 API와 대체 API

1-25

`struct static_key`를 직접 사용하는 방식과 `static_key_true()`·`static_key_false()`는 폐기되었다. `STATIC_KEY_INIT_FALSE`나 `STATIC_KEY_INIT_TRUE`로 구조체 변수를 직접 초기화하는 code도 사용하면 안 된다.

대신 `DEFINE_STATIC_KEY_TRUE()`와 `DEFINE_STATIC_KEY_FALSE()`로 key를 선언하고, 배열에는 `DEFINE_STATIC_KEY_ARRAY_TRUE()` 또는 `DEFINE_STATIC_KEY_ARRAY_FALSE()`를 사용한다. branch site에서는 `static_branch_likely()`와 `static_branch_unlikely()`를 사용한다.

Static key API 교체
폐기대체
struct static_key + STATIC_KEY_INIT_TRUEDEFINE_STATIC_KEY_TRUE
struct static_key + STATIC_KEY_INIT_FALSEDEFINE_STATIC_KEY_FALSE
static_key_truestatic_branch_likely 또는 static_branch_unlikely
static_key_falsestatic_branch_likely 또는 static_branch_unlikely
직접 배열DEFINE_STATIC_KEY_ARRAY_TRUE/FALSE

직접 구조체 API를 typed declaration과 branch helper로 바꾼다.

===========
Static Keys
===========

.. warning::

   DEPRECATED API:

   The use of 'struct static_key' directly, is now DEPRECATED. In addition
   static_key_{true,false}() is also DEPRECATED. IE DO NOT use the following::

	struct static_key false = STATIC_KEY_INIT_FALSE;
	struct static_key true = STATIC_KEY_INIT_TRUE;
	static_key_true()
	static_key_false()

   The updated API replacements are::

	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()

fast path에 드문 기능 넣기

26-50

static key는 GCC 기능과 runtime code patching을 결합해 성능에 민감한 kernel fast path에 거의 사용되지 않는 기능을 낮은 비용으로 포함하게 한다.

예제는 `DEFINE_STATIC_KEY_FALSE(key)`로 기본 false key를 만들고 `static_branch_unlikely(&key)`로 드문 경로를 고른다. `static_branch_enable(&key)`이 true branch를 활성화하고 `static_branch_disable(&key)`이 다시 비활성화한다.

`static_branch_unlikely()` site는 흔한 likely code path에 주는 영향을 가능한 한 작게 만들도록 생성된다.

Static branch 수명 주기
DEFINE_STATIC_KEY_FALSELikely path has minimal instruction cost
static_branch_enablePatch branch siteUnlikely feature active
static_branch_disablePatch backFast path restored

기본값에 최적화된 branch site를 필요할 때 patch한다.

Abstract
========

Static keys allows the inclusion of seldom used features in
performance-sensitive fast-path kernel code, via a GCC feature and a code
patching technique. A quick example::

	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);
	...

The static_branch_unlikely() branch will be generated into the code with as little
impact to the likely code path as possible.

tracepoint 조건 검사 비용

51-67

기존 tracepoint는 conditional branch로 구현되어 각 tracepoint마다 global variable을 확인해야 한다. 이 검사 자체의 overhead는 작지만 global variable이 들어 있는 cache line이 다른 memory access와 공유되고 cache pressure가 커지면 비용이 증가한다.

kernel의 tracepoint 수가 늘수록 이 비용은 더 문제가 될 수 있다. tracepoint는 대부분 disabled 상태이고 직접적인 kernel 기능을 제공하지 않는 경우가 많으므로 dormant 상태의 영향을 최대한 줄이는 것이 중요하다.

tracepoint가 이 작업의 최초 동기였지만 static key facility는 다른 kernel code path에서도 사용할 수 있다.

동기
문제영향
각 tracepoint의 global loadcache pressure와 공유 cache-line 비용
많은 dormant tracepoint기능이 꺼져 있어도 fast path가 검사 비용 부담
적용 범위tracepoint 외의 드문 kernel feature에도 사용 가능

static key가 줄이려는 fast-path 비용이다.


Motivation
==========


Currently, tracepoints are implemented using a conditional branch. The
conditional check requires checking a global variable for each tracepoint.
Although the overhead of this check is small, it increases when the memory
cache comes under pressure (memory cache lines for these global variables may
be shared with other memory accesses). As we increase the number of tracepoints
in the kernel this overhead may become more of an issue. In addition,
tracepoints are often dormant (disabled) and provide no direct kernel
functionality. Thus, it is highly desirable to reduce their impact as much as
possible. Although tracepoints are the original motivation for this work, other
kernel code paths should be able to make use of the static keys facility.

asm goto와 jump label patching

68-95

GCC 4.5는 assembly에서 C label로 분기할 수 있는 `asm goto` statement를 추가했다. 이를 사용하면 memory를 읽지 않고도 기본적으로 taken 또는 not-taken인 branch를 만들고, runtime에 branch site를 patch해 방향을 바꿀 수 있다.

기본 disabled인 `static_branch_unlikely()` 예에서는 true branch의 `printk()`가 실행되지 않는다. x86의 straight-line path에는 단일 5-byte atomic no-op이 놓인다.

branch를 flip하면 straight-line의 no-op을 out-of-line true branch로 가는 jump instruction으로 patch한다. branch 방향을 바꾸는 작업은 비싸지만 이후의 branch 선택은 사실상 무료라는 것이 이 최적화의 핵심 tradeoff다.

이 low-level patching mechanism을 jump label patching이라고 하며 static key facility의 기반이 된다.

Jump label patching
Disabled branchAtomic NOP in straight-line path
Enable keyPatch NOP to jumpOut-of-line true branch
Expensive transitionNear-zero steady-state selection cost

runtime 전환 비용을 한 번 지불해 반복 fast-path 비용을 없앤다.

최적화 tradeoff
동작비용
branch enable/disablecode patching과 동기화 때문에 비쌈
branch site 반복 실행기본 방향에서는 NOP 또는 직접 jump

전환과 steady-state 비용의 관계다.

Solution
========


gcc (v4.5) adds a new 'asm goto' statement that allows branching to a label:

https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html

Using the 'asm goto', we can create branches that are either taken or not taken
by default, without the need to check memory. Then, at run-time, we can patch
the branch site to change the branch direction.

For example, if we have a simple branch that is disabled by default::

	if (static_branch_unlikely(&key))
		printk("I am the true branch\n");

Thus, by default the 'printk' will not be emitted. And the code generated will
consist of a single atomic 'no-op' instruction (5 bytes on x86), in the
straight-line code path. When the branch is 'flipped', we will patch the
'no-op' in the straight-line codepath with a 'jump' instruction to the
out-of-line true branch. Thus, changing branch direction is expensive but
branch selection is basically 'free'. That is the basic tradeoff of this
optimization.

This lowlevel patching mechanism is called 'jump label patching', and it gives
the basis for the static keys facility.

Key 정의와 branch site 사용

96-129

최적화를 사용하려면 `DEFINE_STATIC_KEY_TRUE(key)` 또는 `DEFINE_STATIC_KEY_FALSE(key)`로 key를 먼저 정의한다. key는 global이어야 하므로 stack에 두거나 runtime에 동적으로 할당할 수 없다.

code에서는 `static_branch_unlikely(&key)` 또는 `static_branch_likely(&key)` 조건으로 likely와 unlikely 경로를 나눈다. `DEFINE_STATIC_KEY_TRUE()`와 `DEFINE_STATIC_KEY_FALSE()` 중 어느 것으로 정의했든 두 branch helper 중 어느 것과도 함께 사용할 수 있다.

Key 선언과 사용
항목선택
초기 상태DEFINE_STATIC_KEY_TRUE 또는 DEFINE_STATIC_KEY_FALSE
branch layoutstatic_branch_likely 또는 static_branch_unlikely
저장 위치global 전용; stack·dynamic allocation 불가

초깃값과 branch layout hint는 독립된 선택이다.

Static key label API, usage and examples
========================================


In order to make use of this optimization you must first define a key::

	DEFINE_STATIC_KEY_TRUE(key);

or::

	DEFINE_STATIC_KEY_FALSE(key);


The key must be global, that is, it can't be allocated on the stack or dynamically
allocated at run-time.

The key is then used in code as::

        if (static_branch_unlikely(&key))
                do unlikely code
        else
                do likely code

Or::

        if (static_branch_likely(&key))
                do likely code
        else
                do unlikely code

Keys defined via DEFINE_STATIC_KEY_TRUE(), or DEFINE_STATIC_KEY_FALSE, may
be used in either static_branch_likely() or static_branch_unlikely()
statements.

상태 전환, reference count와 CPU hotplug

130-179

`static_branch_enable(&key)`은 branch를 true로, `static_branch_disable(&key)`은 false로 설정한다. reference-count 방식은 `static_branch_inc()`와 `static_branch_dec()`를 사용한다.

`static_branch_inc()`는 적절한 reference count를 증가시키며 branch를 true로 만들고, `static_branch_dec()`는 감소시키며 false로 만든다. true로 초기화한 key는 `dec()`로 false가 되고 다음 `inc()`로 true가 된다. false 초기 key는 `inc()`로 true, 이후 `dec()`로 다시 false가 된다.

현재 상태와 reference count는 `static_key_enabled()`와 `static_key_count()`로 얻는다. 일반적으로 이 조회는 enable/disable 또는 inc/dec를 보호하는 것과 같은 mutex로 보호해야 한다.

branch 전환은 kernel patch 중 CPU가 새로 들어오는 race를 피하기 위해 CPU hotplug lock을 비롯한 lock을 잡는다. 따라서 hotplug notifier 안에서 일반 static key API를 호출하면 deadlock이 확실하다.

그 특수 context를 위해 `_cpuslocked` 변형인 `static_key_enable_cpuslocked()`, `static_key_disable_cpuslocked()`, `static_branch_enable_cpuslocked()`, `static_branch_disable_cpuslocked()`가 제공된다. 이는 범용 API가 아니며 정확히 해당 context임을 아는 경우에만 사용해야 한다.

key 배열은 `DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count)` 또는 `DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count)`로 정의한다.

상태 관리 API
API효과주의
static_branch_enable/disabletrue/false 직접 설정patching lock 획득
static_branch_inc/decreference count 기반 true/false짝을 맞춰 사용
static_key_enabled/count상태와 count 조회같은 mutex 권장
*_cpuslockedCPU hotplug lock 보유 context범용 사용 금지

직접 상태 전환과 reference-count 전환을 구분한다.

Reference-count 상태
count 0Branch false
static_branch_inccount > 0Branch true
static_branch_deccount returns to 0Branch false

활성 사용자 수가 branch의 논리 상태를 정한다.

Branch(es) can be set true via::

	static_branch_enable(&key);

or false via::

	static_branch_disable(&key);

The branch(es) can then be switched via reference counts::

	static_branch_inc(&key);
	...
	static_branch_dec(&key);

Thus, 'static_branch_inc()' means 'make the branch true', and
'static_branch_dec()' means 'make the branch false' with appropriate
reference counting. For example, if the key is initialized true, a
static_branch_dec(), will switch the branch to false. And a subsequent
static_branch_inc(), will change the branch back to true. Likewise, if the
key is initialized false, a 'static_branch_inc()', will change the branch to
true. And then a 'static_branch_dec()', will again make the branch false.

The state and the reference count can be retrieved with 'static_key_enabled()'
and 'static_key_count()'.  In general, if you use these functions, they
should be protected with the same mutex used around the enable/disable
or increment/decrement function.

Note that switching branches results in some locks being taken,
particularly the CPU hotplug lock (in order to avoid races against
CPUs being brought in the kernel while the kernel is getting
patched). Calling the static key API from within a hotplug notifier is
thus a sure deadlock recipe. In order to still allow use of the
functionality, the following functions are provided:

	static_key_enable_cpuslocked()
	static_key_disable_cpuslocked()
	static_branch_enable_cpuslocked()
	static_branch_disable_cpuslocked()

These functions are *not* general purpose, and must only be used when
you really know that you're in the above context, and no other.

Where an array of keys is required, it can be defined as::

	DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);

or::

	DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);

Architecture jump-label 구현 인터페이스

180-207

architecture가 jump-label 최적화를 사용하려면 몇 가지 함수와 macro를 구현해야 한다. 지원이 없으면 전통적인 load, test, jump sequence로 fallback한다.

`static_key->entry` field가 두 least-significant bit를 사용하므로 `struct jump_entry` table은 최소 4-byte alignment를 가져야 한다.

필요한 interface는 `HAVE_ARCH_JUMP_LABEL` 선택, `JUMP_LABEL_NOP_SIZE`, `arch_static_branch()`, `arch_static_branch_jump()`, `arch_jump_label_transform()`, `struct jump_entry`다. 원문은 x86 Kconfig, `arch/x86/include/asm/jump_label.h`, `arch/x86/kernel/jump_label.c`를 구현 예로 든다.

Architecture interface
항목예시 위치
HAVE_ARCH_JUMP_LABELarch/x86/Kconfig
JUMP_LABEL_NOP_SIZEarch/x86/include/asm/jump_label.h
arch_static_brancharch/x86/include/asm/jump_label.h
arch_static_branch_jumparch/x86/include/asm/jump_label.h
arch_jump_label_transformarch/x86/kernel/jump_label.c
struct jump_entryarch/x86/include/asm/jump_label.h

jump label 지원 architecture가 제공할 항목이다.

4) Architecture level code patching interface, 'jump labels'


There are a few functions and macros that architectures must implement in order
to take advantage of this optimization. If there is no architecture support, we
simply fall back to a traditional, load, test, and jump sequence. Also, the
struct jump_entry table must be at least 4-byte aligned because the
static_key->entry field makes use of the two least significant bits.

* ``select HAVE_ARCH_JUMP_LABEL``,
    see: arch/x86/Kconfig

* ``#define JUMP_LABEL_NOP_SIZE``,
    see: arch/x86/include/asm/jump_label.h

* ``__always_inline bool arch_static_branch(struct static_key *key, bool branch)``,
    see: arch/x86/include/asm/jump_label.h

* ``__always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)``,
    see: arch/x86/include/asm/jump_label.h

* ``void arch_jump_label_transform(struct jump_entry *entry, enum jump_label_type type)``,
    see: arch/x86/kernel/jump_label.c

* ``struct jump_entry``,
    see: arch/x86/include/asm/jump_label.h

x86_64 getppid instruction 분석

208-286

분석 예는 `getppid()` system call에 `static_branch_unlikely(&key)` 조건과 `printk()` true branch를 추가한다. GCC가 jump label을 생성한 instruction sequence와 최적화가 없는 sequence를 비교한다.

jump-label version의 기본 disabled path에는 `jmpq`의 5-byte 자리만 있고 boot 때 5-byte atomic no-op으로 patch된다. true branch의 `printk()`는 out-of-line에 놓이며 활성화 시 jump가 그쪽으로 향한다.

최적화가 없으면 fast path가 global key를 읽는 `mov`, 값을 검사하는 `test`, true branch로 가는 `jne`를 매번 실행한다. disabled case에서 이 세 instruction은 6+2+2=10 byte이고 jump-label 자리 5 byte와 비교하면 5 byte가 추가된다.

padding까지 포함하면 이 작은 함수에서 jump-label code가 instruction memory 16 byte를 절약한다. 비최적화 함수가 80 byte이므로 instruction footprint가 20% 줄어든다.

분기 거리가 2-byte jump로 닿는다면 5-byte no-op 대신 2-byte no-op을 써서 더 줄일 수 있지만, 문서 시점에는 optimal no-op size가 구현되지 않고 크기가 hard-coded되어 있다.

Disabled path 비교
구현조건 처리크기 영향
jump label5-byte NOP 또는 jmp 0 자리기준 5 byte
일반 branchmov 6 + test 2 + jne 210 byte, 5 byte 추가
함수 전체padding 포함16/80 byte, 20% 절약

x86_64 예제의 반복 fast-path instruction 차이다.

getppid fast path
Jump label disabledNOPNormal getppid body
Jump label enabledPatched JMPOut-of-line printkNormal body
No optimizationLoad keyTestConditional jump

jump-label 유무에 따른 조건 평가 경로다.

5) Static keys / jump label analysis, results (x86_64):


As an example, let's add the following branch to 'getppid()', such that the
system call now looks like::

  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;
  }

The resulting instructions with jump labels generated by GCC is::

  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>

Without the jump label optimization it looks like::

  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

Thus, the disable jump label case adds a 'mov', 'test' and 'jne' instruction
vs. the jump label case just has a 'no-op' or 'jmp 0'. (The jmp 0, is patched
to a 5 byte atomic no-op instruction at boot-time.) Thus, the disabled jump
label case adds::

  6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 addition bytes.

If we then include the padding bytes, the jump label code saves, 16 total bytes
of instruction memory for this small function. In this case the non-jump label
function is 80 bytes long. Thus, we have saved 20% of the instruction
footprint. We can in fact improve this even further, since the 5-byte no-op
really can be a 2-byte no-op since we can reach the branch with a 2-byte jmp.
However, we have not yet implemented optimal no-op sizes (they are currently
hard-coded).

Scheduler pipe-test 실측 결과

287-328

scheduler path에 static key API 사용이 많으므로 `pipe-test`, 즉 `perf bench sched pipe`로 성능 향상을 확인할 수 있다. 원문 결과는 Linux 3.3.0-rc2에서 50회 실행한 성능 counter다.

jump label disabled 결과는 task-clock 855.700314, cycles 1,474,374,262, instructions 1,178,049,567, branches 208,368,926, branch-misses 5,569,188, elapsed 1.601607384초다.

jump label enabled 결과는 task-clock 841.043185, cycles 1,432,559,428, instructions 1,175,363,994, branches 206,859,359, branch-misses 4,884,119, elapsed 1.579384366초다. 두 경우 모두 약 200,000번의 context switch, 0 CPU migration, 487 page fault를 기록했다.

문서가 계산한 개선은 branch 0.7%, branch-miss 12%, instruction 0.2%, cycle 2.8%, elapsed time 1.4% 감소다. branch 수를 줄이는 최적화이므로 가장 큰 절약이 branch-miss에서 나타나는 것이 예상과 맞는다.

pipe-test 개선
지표절감
branches0.7%
branch-misses12%
instructions0.2%
cycles2.8%
elapsed time1.4%

원문이 요약한 jump-label enabled의 절감률이다.

주요 raw counter
지표disabledenabled
cycles1,474,374,2621,432,559,428
instructions1,178,049,5671,175,363,994
branches208,368,926206,859,359
branch-misses5,569,1884,884,119
elapsed seconds1.6016073841.579384366

50회 pipe-test에서 disabled와 enabled를 비교한다.

Since there are a number of static key API uses in the scheduler paths,
'pipe-test' (also known as 'perf bench sched pipe') can be used to show the
performance improvement. Testing done on 3.3.0-rc2:

jump label disabled::

 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% )

jump label enabled::

 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

The percentage of saved branches is .7%, and we've saved 12% on
'branch-misses'. This is where we would expect to get the most savings, since
this optimization is about reducing the number of branches. In addition, we've
saved .2% on instructions, and 2.8% on cycles and 1.4% on elapsed time.