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, 0x8

rsp

実際に再現してみる

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			retq

DWARF 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, 0x58
0x140001233      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