コンパイル・リンクと実行時の最適化
CPythonでコードの生成と実行を分け、バイトコードを確認します。次にClangでCの二つのファイルをコンパイル・リンクし、最後にNode.jsの実行中の最適化と解除を確認します。
説明
ソースコードから結果が出るまでには、構文の解析、実行用コードへの変換、外部の定義との結合、実際の計算といった段階があります。言語や実行系によって、使う表現と処理の時点が異なります。
CPythonでコードの生成と実行を分け、バイトコードを確認します。次にClangでCの二つのファイルをコンパイル・リンクし、最後にNode.jsの実行中の最適化と解除を確認します。
1計算するソース
calculateはa + b * 2を返し、3と4で呼ばれます。
cat calculate.py翻訳するソースを表示掛け算が先にまとまるため、計算結果は3+8=11です。ただし、ソースを読み取っただけでは関数の定義や呼出しは実行されません。
2コードを生成する段階
compile_demo.pyはソースをcompileでコードオブジェクトへ変換します。compileモードではexecを呼ばず、用意した名前空間を表示します。
python3 compile_demo.py compileコード生成までで止めるcodeオブジェクトは作られていますが、名前空間は空です。コンパイルしただけでは、ソース内の関数定義・代入・printは実行されません。
3生成したコードを実行する
同じソースからコードを作り、今度はexecで名前空間へ適用します。
python3 compile_demo.py run生成コードを実行し保存値を表示ソース内のprintが11を表示し、名前空間にもresult=11が保存されます。コード生成と、そのコードを使って状態を変える実行は別の段階です。
4CPythonの中間命令
関数calculateのコードオブジェクトをdisで読み、バイト位置・命令名・引数を表示します。
python3 compile_demo.py bytecode関数のバイトコードを表示bと2の乗算を行った後、aとの加算を行い、値を返します。これらはCPythonが使う中間命令であり、CPUの機械語ではありません。表示はCPython 3.15.0のもので、版によって命令や配置は変わります。 bytesは実際のバイト列、左端の数値はその中の位置、argは命令の数値引数です。LOAD_SMALL_INTの引数2が定数2に対応します。省略されたキャッシュ領域があるため、表示位置は連続しません。
5演算の優先順位と呼出し順
各値を返す前に名前を表示する関数を使い、式の評価順を確認します。
cat evaluation.py呼出しを記録する式
python3 evaluation.py優先順位・評価順・短絡を確認呼出しはA、B、Cの順ですが、値は3+(4*2)=11として計算されます。演算子の優先順位と、値を取得する呼出し順は別です。False andの右辺は呼ばれず、unusedの表示はありません。
6別ファイルの関数を参照するCコード
main.cはaddを宣言して呼び、定義はadd.cへ置いてあります。
head -n 20 main.c add.c呼出し側と定義側を表示main.cの宣言は引数と戻り値の型を知らせますが、関数本体は含みません。別々にコンパイルした後、呼出し先を結び付ける必要があります。
7オブジェクトの未解決名
Apple Clang、arm64、macOS 15.2 SDKの設定で、二つのオブジェクトファイルを作ってシンボルを表示します。
clang -O1 -c main.c -o main.o呼出し側をオブジェクト化
clang -O1 -c add.c -o add.o関数定義をオブジェクト化
nm -u main.o未解決の参照
nm -g add.o外部へ公開する定義main.oには_addと_printfへの未解決の参照があります。add.oでは_addがコード領域の定義としてTで表示されます。この例はmacOS/arm64で、外部シンボル名に先頭の下線が付きます。
8定義を渡さずリンクした場合
main.oだけで実行ファイルを作ろうとし、終了コードと未解決シンボルの行を表示します。
clang main.o -o incomplete定義側を含めずリンク 2> link-error.txt
status=$?終了コードを保持
printf 'exit=%s
' "$status"
grep -E未解決名の行を抽出 '"_add"|symbol.s. not found' link-error.txtaddの定義が見つからず、リンクは失敗します。コンパイル時に宣言を知っていても、実行ファイルへ結び付ける実体があるとは限りません。
9両方をリンクして実行
定義を持つadd.oも渡し、作った実行ファイルを起動します。
clang main.o add.o -o combined参照と定義をリンク
./combined実行ファイルを起動3+4の7が出ます。リンクでは、外部名の参照と定義を結び、必要な再配置を反映します。printfには標準ライブラリ側の定義を利用します。
10同じ関数のCPU命令
add.cからarm64のアセンブリを出力し、addの関数部分を表示します。
clang -O1 -S add.c -o add.sCPU向けアセンブリを出力
sed -n '/_add:/,/cfi_endproc/p' add.s関数の命令部分を表示add w0, w1, w0で32ビットの引数を加え、retで戻ります。CPythonのバイトコードとは異なる、対象CPU向けの命令です。w0は戻り値にも使われます。
11実行中の最適化を観察する試料
Node.jsのV8でaddへ数値を渡した後、最適化を要求し、最後に文字列を渡します。%で始まる行は検証用の内部機能で、通常のJavaScript構文ではありません。
cat jit.js最適化を明示的に要求する試料実行中の型の観測に基づいて機械語へ最適化する方式がJITです。この試料では最適化が起きるタイミングを内部機能で明示しています。通常のアプリケーションで使うコードではありません。
12最適化と前提の変化
V8の最適化記録を採取し、addのコンパイル完了、解除理由、計算結果だけを抽出します。
node --allow-natives-syntax --trace-opt --trace-deopt jit.js内部機能を許可し最適化記録を採取 > jit.trace記録を保存
python3 summarize_jit.py jit.trace対象関数のイベントと結果を抽出数値の観測後にaddが最適化され、数値では11を返します。文字列を渡すとnot a Smiという理由で解除され、それでもJavaScriptの意味に従ってabを返します。最適化の前提が崩れた場合の処理が必要で、翻訳の費用もあるため、単発の実行が必ず速くなるという意味ではありません。
まとめ
コンパイルしたコードを実行する段階で、変数や出力が実際に変化します。
バイトコードとCPU命令は異なる表現です。外部の定義を使うコードではリンク時の名前解決も必要です。
JITは実行中に最適化します。観測した条件に合わなくなれば解除される場合があり、結果の意味を保つ仕組みが必要です。
操作履歴と解説を振り返れます。
シェル / CPython・Clang・Node.js
cat calculate.pydef calculate(a, b):
return a + b * 2
result = calculate(3, 4)
print(result)python3 compile_demo.py compilecompiled: code defined: []
python3 compile_demo.py run11 result in namespace: 11
python3 compile_demo.py bytecodebytes: 80 00 00 00 56 01 5d 02 2a 05 00 00 00 00 00 00 00 00 00 00 2a 00 00 00 00 00 00 00 00 00 00 00 21 00 00 RESUME arg=0 04 LOAD_FAST_BORROW_LOAD_FAST_BORROW arg=1 a, b 06 LOAD_SMALL_INT arg=2 08 BINARY_OP arg=5 * 20 BINARY_OP arg=0 + 32 RETURN_VALUE arg=None
cat evaluation.py
python3 evaluation.pydef operand(name, value):
print("evaluate", name)
return value
print("result",operand("A",3)+operand("B",4)*operand("C",2))
print("short",False and operand("unused",99))
evaluate A
evaluate B
evaluate C
result 11
short Falsehead -n 20 main.c add.c==> main.c <==
#include <stdio.h>
int add(int a, int b);
int main(void) {
printf("%d\n", add(3, 4));
return 0;
}
==> add.c <==
int add(int a, int b) {
return a + b;
}clang -O1 -c main.c -o main.o
clang -O1 -c add.c -o add.o
nm -u main.o
nm -g add.o_add _printf 0000000000000000 T _add
clang main.o -o incomplete 2> link-error.txt
status=$?
printf 'exit=%s
' "$status"
grep -E '"_add"|symbol.s. not found' link-error.txtexit=1 "_add", referenced from: ld: symbol(s) not found for architecture arm64
clang main.o add.o -o combined
./combined7
clang -O1 -S add.c -o add.s
sed -n '/_add:/,/cfi_endproc/p' add.s_add: ; @add .cfi_startproc ; %bb.0: add w0, w1, w0 ret .cfi_endproc
cat jit.jsfunction add(a, b) { return a + b; }
%PrepareFunctionForOptimization(add);
console.log("phase warmup", add(1, 2), add(3, 4));
%OptimizeFunctionOnNextCall(add);
console.log("phase number", add(5, 6));
console.log("phase string", add("a", "b"));node --allow-natives-syntax --trace-opt --trace-deopt jit.js > jit.trace
python3 summarize_jit.py jit.tracephase warmup 3 7 compiled add: TURBOFAN_JS phase number 11 deoptimized add: not a Smi phase string ab
演習完了