Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Compiler/JIT optimizations & escape analysis
Search
saiya_moebius
July 05, 2019
Programming
2
350
Compiler/JIT optimizations & escape analysis
コンパイラ・JIT が行っている各種の最適化や JIT の振る舞い(の一部)を概説し、
その背景にある Escape Analysis についても軽く解説します。
saiya_moebius
July 05, 2019
Tweet
Share
More Decks by saiya_moebius
See All by saiya_moebius
async 完全理解 - 全ての async は Promise に通ず / Guide to async, await
saiya_moebius
1
490
エムスリーの Over 300 マイクロサービスを支えるマルチクラウドのネットワーク設計 / M3 cloud network infrastructure for over 1000 microservices
saiya_moebius
0
300
TypeScript の型システム / Type system of the TypeScript
saiya_moebius
10
3.2k
垂直スケールの果ての db.r4.16xlarge で得た教訓 / What happened on vertically scaled 16xlarge DB
saiya_moebius
4
3.9k
DNS を 15 分で雑に知る / grasp DNS in 15 minutes
saiya_moebius
0
120
分散トレーシングの技術選定・OSS 貢献, Stackdriver Trace での性能可視化・改善 / Distributed Tracing case study
saiya_moebius
10
6.4k
RDBMS in Action
saiya_moebius
56
23k
Kubernetes こわくないよ!
saiya_moebius
1
5.6k
How to setup Gradle to improve legacy Java system
saiya_moebius
1
2.6k
Other Decks in Programming
See All in Programming
距離関数を極める! / SESSIONS 2024
gam0022
0
280
OSSで起業してもうすぐ10年 / Open Source Conference 2024 Shimane
furukawayasuto
0
100
Better Code Design in PHP
afilina
PRO
0
120
Nurturing OpenJDK distribution: Eclipse Temurin Success History and plan
ivargrimstad
0
880
Quine, Polyglot, 良いコード
qnighy
4
640
Realtime API 入門
riofujimon
0
150
受け取る人から提供する人になるということ
little_rubyist
0
230
ECS Service Connectのこれまでのアップデートと今後のRoadmapを見てみる
tkikuc
2
250
レガシーシステムにどう立ち向かうか 複雑さと理想と現実/vs-legacy
suzukihoge
14
2.2k
エンジニアとして関わる要件と仕様(公開用)
murabayashi
0
280
Amazon Bedrock Agentsを用いてアプリ開発してみた!
har1101
0
330
シールドクラスをはじめよう / Getting Started with Sealed Classes
mackey0225
4
640
Featured
See All Featured
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
16
2.1k
Designing for humans not robots
tammielis
250
25k
RailsConf 2023
tenderlove
29
900
Designing on Purpose - Digital PM Summit 2013
jponch
115
7k
The World Runs on Bad Software
bkeepers
PRO
65
11k
YesSQL, Process and Tooling at Scale
rocio
169
14k
Intergalactic Javascript Robots from Outer Space
tanoku
269
27k
Practical Orchestrator
shlominoach
186
10k
Build your cross-platform service in a week with App Engine
jlugia
229
18k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
131
33k
For a Future-Friendly Web
brad_frost
175
9.4k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Transcript
いまどきのコンパイラ・JIT の最適化と Escape Analysis M3 Tech Talk - July 5
2019 Seiya Yazaki 1
Escape Analysis? -> オブジェクトのスコープ(⽣存区間)を⾃動判定すること 現代的なコンパイラ最適化や JIT の振る舞いを知る上で有⽤な概念。 今回は各種の最適化や JIT の振る舞いを概説し、
その背景にある Escape Analysis についても触れる。 2
オブジェクトのスコープ? オブジェクトのスコープ と 変数のスコープ は違う 3
オブジェクトのスコープが変数と⼀致する例 何かの関数 () { val something = new Object(); return;
} 変数のスコープ: 宣⾔から関数の終わりまで オブジェクトのスコープ: 宣⾔から関数の終わりまで 4
スコープが⼀致しない例 val グローバル変数; 何かの関数 () { val something = new
Object(); グローバル変数 = something; return something; } 変数のスコープ: 宣⾔から関数の終わりまで オブジェクトのスコープ: new してからずっと⽣存 ( escape している) 5
オブジェクトのスコープの判定 変数のスコープはプログラム⾔語の仕様として明確。 オブジェクトのスコープは必ずしも⾃明ではない。 オブジェクトが関数やクラスの外側から参照されうる(escape している)かを判定するのが Escape Analysis。 6
1: 最適化における Escape Analysis の必要性 7
最適化の前提としての Escape Analysis オブジェクトが escape しないことを前提とする最適化⼿法がいっぱいある: オブジェクトの展開 (スタック割付け, Scalar replacement)
オブジェクトのインライン化 キャッシュ write back の省略 ロック・Atomic メモリ操作の省略 ラムダ式のクロージャーの最適化 ... この章では、これらの最適化について概説。 8
現代的 CPU・メモリの階層型アーキテクチャ CPU はメモリを直接読み書きするのではない。 演算対象の値は register に格納し、L1/L2/L3 cache 経由でメモリと同期する。 さらに
CPU コア・ソケット間では同期操作も必要である。 図の引⽤元: CPU Cache Flushing Fallacy 9
メモリ上のオブジェクトの読み書きは遅い メモリ(DRAM)の読み書きは register の 50〜100 倍遅い。 しかし、近代的なプログラミング⾔語は オブジェクトがメモリに割り当てられる前提の⾔語仕様。 さらにメモリ上のオブジェクトの読み書きには各種オーバーヘッドがある: メモリアロケーター,
ロック, 参照カウント, GC, キャッシュの write back, ... なので極⼒メモリの読み書きを最⼩化したい。 10
オブジェクトのインライン化 (スタックへの割当て) オブジェクトをスタック上の変数で実現してしまう最適化: class Hoge { val value: int }
val hoge = new Hoge(123); ↓ スタック割付け最適化 val hoge のvalue: int = 123; // hoge.value の読み書きはこれを使う リスト等のイテレーターの最適化 等で無意識にかなりの恩恵を受けている。 11
インライン化の前提としての Escape Analysis オブジェクトとしての操作が必要な場合、インライン化はできない。 参照の⽐較、型情報の取得、動的プログラミング (リフレクション) など...。 関数の外部にオブジェクトが露出してしまう (escape する)
と上記の保証が不可能。 よって、インライン化は escape していないことが前提になる。 12
⼊れ⼦オブジェクトのインライン化 (Scalar replacement) 同様に、オブジェクトの中のオブジェクトを展開する最適化もある: class SomethingId { val raw: int
} class MyObject { private val id: SomethingId } ↓ インライン化 class MyObject { private val id のraw: int // SomethingId#raw の読み書きはこれを使う } Value object (プリミティブ型のラッパー)やタプル的な型( RGB や Vector2D など)で恩恵を 受けていることがあったりする。 13
オブジェクトの可視性 ⾔語によっては、スレッドをまたぐオブジェクト受け渡しに⼀定の保証がある (JVM の safe publication , Golang の channel
, Apple の GCD など)。 例: サーバーサイドや GUI の実装でありがちな処理: 1. new Something() する 2. それをキューに⼊れる 3. 別スレッドが上記オブジェクトを参照する 1 のオブジェクトへの書き込みが 3 のスレッドから⾒えることが保証されてほしい。 14
メモリモデルとキャッシュのトレードオフ ⼀般にメモリへの書き込みは register や L1/L2/L3 cache に貯めてから書き込む。 しかしそれでは new Something()
によるメモリ書き込み(の⼀部)が別スレッドに⾒えない といった事象が発⽣しうる。 したがって、メモリモデルの保証を満たすためには cache からの write back や、CPU コア間 の同期(MESIF, MOESI といった同期プロトコル)の通信が発⽣する。 15
他スレッドから⾒えないオブジェクトの最適化 他スレッドから⾒えないならば、register や L1/L2/L3 cache からメモリに書き戻す必要はな い。 オブジェクトを CPU コア間で同期する必要もない。
なので escape しないオブジェクトについてはメモリ書き込み処理を⾼速化できる。 ( ついでに swift の ARC のような参照カウントも省略出来る ) 16
ラムダ式のクロージャーに起因するキャプチャー function something() { val x = 123; function lambda()
{ x = 456; } } ラムダ式は外側の変数を読み書きする事ができる (モダンな⾔語なら)。 上記の例における x 変数は something 関数のローカル変数だが、 もし lambda インスタンスが escape するならば、 x も something 関数のスコープを超 えて参照される (キャプチャー)。なので x がメモリ上に配置される。 escape しないと断定できると、変数をスタック上に置いたままにできるのでかなり⾼速にな る。 17
まとめ: Escape Analysis と最適化の関係 オブジェクトの展開 (スタック割付け, Scalar replacement) オブジェクトのインライン化 キャッシュ
write back の省略 ロック・Atomic メモリ操作の省略 ラムダ式のクロージャーの最適化 ... これらの最適化は escape しないことが前提。 なのでコンパイラ・JIT は Escape Analysis する必要がある。 18
2: Escape Analysis と⾔語機能の関係 19
Escape するかどうかは⾃明ではない 何らかの関数 () { val something = new Something();
something.foobar(); return; } something は関数のスコープから escape するか? 20
メソッド・プロパティの実装に依存 何らかの関数 () { val something = new Something(); something.foobar();
return; } class Something { foobar() { baz.onClick = () => { // ラムダ式 alert("Hello World!"); } } } この場合 something は escape しない。 21
メソッド・プロパティの実装に依存 何かの関数 () { val something = new Something(); something.foobar();
return; } class Something { foobar() { baz.onClick = () => { // ラムダ式 this.barbaz(); // ここが something を参照 } } } この場合は escape する (onClick に⼊れた lambda が this 経由で Object を escape)。 22
Escape Analysis とメソッドの実装依存 メソッドの呼び出し先のメソッドの ... 経由で escape する といった可能性があるので厄介。 どうやって
escape しないと断定するか? 23
構造体 (struct) の活⽤ C, C++, C#, Swift 等の構造体( struct )は
Escape Analysis に優しい。 これらの⾔語の struct の寿命は変数のスコープと常に⼀致するため、呼び出し先のメソッ ドの実装云々に関係なく escape しないことを断定できる。 ( なお Golang の struct は escape 可能であり、このようなメリットはない ) なお、 struct は変数間の代⼊でコピー渡しのオーバーヘッドが発⽣しうることもあり、 class などの⾔語機能より常に優先して使うべきということではない。 24
コンパイル時の情報に基づく解析 C, C++, Swift, golang のように機械語へコンパイルする⾔語では、 当然ながらコンパイル時の情報のみで Escape Analysis する。
たいては以下のような判定をしている: コンパイル時に呼び出し先が確定しない関数の引数に渡しているならば NG その関数の中で escape するかもしれない 当該オブジェクトの参照をグローバル変数・インスタンス変数などに⼊れていれば NG なお、golang ならば -gcflags='-m' で Escape Analysis とインライン化の結果が分かりや すく出⼒される。 25
Compiler directive ⼀部の⾔語には Escape Analysis を助けるための⾔語機能がある: C, C++ における noescape
や clang::noescape 属性 Go の //go:noescape これらを明⽰することで Escape しないことをコンパイラに伝えられる。 ただし使い⽅が間違っていると何がおきてもおかしくないので、⾃⼰責任で。 26
コンパイル時の解析を妨げる、ありがちな⾔語機能 呼び出し先の関数が確定しないケースで、どうにもならなくなりがち: ポリモフィズム・オーバーライド オーバーライドできる = 引数を escape する実装になっているかもしれない ラムダ式や関数ポインタ ラムダを呼び出す側にはラムダの実装がわからない
= 引数を escape しうる これらがあると、escape するものと想定せざるをえず、各種最適化ができない (ことが多い)。 27
実⾏時情報に基づく最適化 (JIT) JIT であれば、実⾏時の情報を使うことでさらなる最適化が可能。 こういった場合に、呼び出し先の関数の実装が⼀意に確定する: 継承やオーバーライドが可能だが実際にはされていないメソッド 中⾝が常に決め打ちのラムダ式や関数ポインタ なので呼び出し先の関数の実装内容に依存した Escape Analysis
ができる。 少なくとも JVM や V8 はこのような最適化をしている (後述の参考資料を参照)。 28
Deoptimization (脱最適化) 実⾏時の挙動によって、最適化の前提が崩れることがある: 後からロードされた class によるオーバーライド JVM は class を遅延ロードする仕様
動的コード⽣成によるオーバーライド Aspect Oriented Programing (トランザクション制御など) シリアライザや O/R mapper 等 関数の実装の動的な差し替え mock ライブラリ等 ⾼度な⾃⼰書き換え型プログラムなど JIT はこういったケースを検知し、最適化の前提が崩れているケースでは最適化前のコードに 戻すことで正しく動作する (deoptimization)。 Deoptimization による性能低下は micro benchmark では⾒逃されがちな要素。 29
"早すぎる最適化は諸悪の根源" -- ドナルド・クヌース 性能・最適化の議論で⼀般に陥りがちな過ちに対する警句。 性能・最適化の議論をするときは必ず⼼に留めておくべき。 30
とはいえ、スコープは⼩さくすると良い 「スコープを⼩さくする」のはプログラミングのベストプラクティス。 Escape analysis や最適化関係とは独⽴に、オブジェクトの⽣存区間を不必要に⼤きくしない コーディングには意味がある。 「変数のスコープを⼩さくする」だけでなく「オブジェクトのスコープを⼩さくする」のもメ ンテナンス性・バグ予防・読みやすさに効く。 31
Appendix: Escape Analysis 参考資料 いろいろな⾔語処理系の公式情報: JVM (HotSpot): EscapeAnalysis - OpenJDK
wiki JVM (Java, Kotlin/JVM, Scala) の JIT における Escape Analysis Escape Analysis に限らず、JVM (HotSopt) の JIT はとっても強い V8 (JS): Escape Analysis in V8 GraalVM: Under the hood of GraalVM JIT optimizations Golang: Go Escape Analysis Flaws Swift: github.com/apple/swift の EscapeAnalysis.cpp PyPy: Escape Analysis in PyPy's JIT CPython はおそらく Escape Analysis していない CRuby: Ruby 2.6 JIT - Progress and Future CRuby は Escape Analysis をまだ実装していない ※ 処理系の進歩が資料に反映されていない可能性が⼤いにある 32
Appendix: Happens(-ed) Before 並列処理をきちんと考慮しているプログラミング⾔語では Memory Model が仕様として定義 されている。 Memory model
仕様が定義されている⾔語の例: Java (>= 5), C++11, C11, C#, Golang 最近の model は Happens Before という概念で構成されていることが多い。 Happens Before はプログラム上の操作の半順序関係 (全順序ではない)。 順序が定義されない 2 操作が同じオブジェクト・メモリを操作してるとスレッドセーフでな い。 33