Profiling を支える
高速で正確な Unwind
石 立行
About me
石 立行 (ishitatsuyuki)
東京大学 理学部情報科学科 3 年
Linux のグラフィックエコシステムを
中心に開発中
低レイヤーが好き

みなさん、
プロファイラ
を使ったことはありますか?
みなさん、
プロファイラ
がどう動いているか、知っていますか?
プロファイラが動く仕組み
① 1ms 単位で高速に実行に割り込む

② 割り込む際に、スタックトレースを取得


Stack Chart
スタックトレースの取得
スタート地点:
割り込み時のレジスタ
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};スタックトレースの取得
return をシミュレートし、
呼び出し元関数の状態を再現
考え方:
struct pt_regs
callee()
struct pt_regs
caller()
Unwind
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
rbp, rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
| r14 |
rbp
rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
| r14 |
| r13 |
rbp
rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
| r14 |
| r13 |
| r12 |
rbp
rsp
実際に再現してみる
caller:
call callee
callee:
push rbp
mov rbp, rsp
push r14
push r13
push r12
sub rsp, 0x8| caller |
| rbp |
| r14 |
| r13 |
| r12 |
rbp
rsp
この時点での rsp は?
old_rsp
= rsp + 40
= rbp + 16
rbp からのオフセットは
この間固定
様々な関数パターン
- Frame Pointer
- No Frame Pointer
- Base Pointer
- DRAP
- Signal Frame
rbp=*rbp, rsp=rbp+16
rbp=*(rsp+A), rsp=rsp+B
rbp=*(rbp+A), rsp=rbp+B
rbp=*rbp, rsp=*(rbp+A)
rbp=*(rsp+A), rsp=*(rsp+B)
これらをエンコーディングするにはどうすればよいのか?
DRAP
4c 8d 54 24 08 lea 0x8(%rsp),%r10
48 83 e4 c0 and $0xffffffffffffffc0,%rsp
41 ff 72 f8 pushq -0x8(%r10)
55 push %rbp
48 89 e5 mov %rsp,%rbp
(more pushes)
41 52 push %r10
...
41 5a pop %r10
(more pops)
5d pop %rbp
49 8d 62 f8 lea -0x8(%r10),%rsp
c3 retqDWARF CIE/FDE
基本の考え方: 「命令のアドレス」と、「計算式」の表

利点: ABI を気にせず、何でも表せる
例えばこんなのも表せる: rsp + 8 + ((((rip & 15) >= 11) ? 1 : 0) << 3)
欠点: Turing-Complete
でも、DWARF って
デバッグ情報じゃないのか?
- DWARF 自身は、デバッグ情報のスタンダード
- Itanium x64 ABI では、「例外処理用情報」として
実行ファイルの一部に - Windows x64 ABI や、AArch64 の実装でも同様
デバッグシンボルの有無によらず、
Unwind が可能
Linux のプロファイラ事情
現状、Linux では 2 種類の Unwind 実装が選べる
- Frame Pointer
- DWARF
DWARF はもちろんより強力だが...
- Unwind の速度が Frame Pointer に比べ桁違いで遅い
- ディスクに書き込まれるデータサイズがデカい
Linux のプロファイラ事情
DWARF には、「遅い」というスティグマがある
- Google 社全体で Frame Pointer ベースのコンパイルに切り替え
- Fedora の新バージョンでも、全パッケージ
Frame Pointer 付きでのコンパイルを採択
この犠牲は必要なのだろうか?
DWARF を早くする工夫 (1)
RIP, RBP, RSP のみを計算
- 大抵の場合、呼び出し元の RSP は RBP または RSP から計算可能
- 他のレジスタの計算を省略
- 計算ステップのコストが数倍下がる
- 例外有り
→フォールバックが必要
DWARF を早くする工夫 (2)
アドレスに対応する計算式を
キャッシュで保持
プロファイリングにおいて、多くの
スタックは繰り返しヒットする
509 エントリのキャッシュでヒット率 8 – 9 割
DWARF を早くする工夫 (2)
/// For all of these: return address is *(new_sp - 8)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnwindRuleX86_64 {
/// (sp, bp) = (sp + 8, bp)
JustReturn,
/// (sp, bp) = if is_first_frame (sp + 8, bp) else (bp + 16, *bp)
JustReturnIfFirstFrameOtherwiseFp,
/// (sp, bp) = (sp + 8x, bp)
OffsetSp { sp_offset_by_8: u16 },
/// (sp, bp) = (sp + 8x, *(sp + 8y))
OffsetSpAndRestoreBp {
sp_offset_by_8: u16,
bp_storage_offset_from_sp_by_8: i16,
},
/// (sp, bp) = (bp + 16, *bp)
UseFramePointer,
}DWARF を早くする工夫 (2)
キャッシュヒットすれば:
- 計算式を探すための二分探索 → 不要
- DWARF のパース → 不要
残るコストは計算式の実行だけ
高速な DWARF ベースの実装
Firefox の開発者による perf (Linux のプロファイラ) の代替実装
オーバーヘッド: 3% 程度 → Fast!
Unwind 成功率: 99% 以上 → Reliable!
残りの 1% → 実はカーネルのバグ
DWARF Unwinding は、実用的に実装可能
SEH Unwinding
アイデア: x64 の機械語に対応する仮想命令を用意
(のサブセット)
.PUSHREG R13
.PUSHREG R14
.PUSHREG R15
.ALLOCSTACK 0x20
.SAVEREG RBX, 0x40
.SAVEREG RBP, 0x48
.SAVEREG RSI, 0x50
.SAVEREG RDI, 0x580x140001233 2 push r13
0x140001235 2 push r14
0x140001237 2 push r15
0x140001239 4 sub rsp, 0x20
0x14000123d 3 mov rsi, r9
[...]
0x1400012a7 4 movzx eax, r15b
0x1400012ab 4 mov cr8, rax
0x1400012af 5 mov rbx, qword [rsp + 0x40]
0x1400012b4 5 mov rbp, qword [rsp + 0x48]
0x1400012b9 5 mov rsi, qword [rsp + 0x50]
0x1400012be 5 mov rdi, qword [rsp + 0x58]Parse はそこまで難しくない
SEH Unwinding
せっかくなのでこれを samply に実装

ORC
DWARF の「表」はそのまま、
計算式の部分を簡略化
.text+325d: sp:(und) bp:(und) type:call end:0
.text+3260: sp:sp+8 bp:(und) type:call end:0
.text+3262: sp:sp+16 bp:(und) type:call end:0
.text+3267: sp:sp+24 bp:(und) type:call end:0
.text+326b: sp:sp+32 bp:prevsp-32 type:call end:0
.text+326f: sp:sp+40 bp:prevsp-32 type:call end:0
.text+3273: sp:sp+96 bp:prevsp-32 type:call end:0
.text+331d: sp:sp+40 bp:prevsp-32 type:call end:0
.text+331e: sp:sp+32 bp:prevsp-32 type:call end:0
.text+331f: sp:sp+24 bp:(und) type:call end:0
.text+3321: sp:sp+16 bp:(und) type:call end:0
.text+3323: sp:sp+8 bp:(und) type:call end:0式は reg + offset の形のみ
Linux カーネルで採用
標準の統一は可能か
DWARF は複雑で Linux カーネルへの
統合がリジェクトされた
→複雑でない代替は存在するのか?
複雑でない代替は存在するが、
すべての ABI に適合するのは難しい
Questions?
Profiling を支える高速で正確な Unwind
By Tatsuyuki Ishi
Profiling を支える高速で正確な Unwind
- 171