一覧PG-19-1 · 翻訳処理とメモリの入口

コンパイル・リンクと実行時の最適化

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.txt

addの定義が見つからず、リンクは失敗します。コンパイル時に宣言を知っていても、実行ファイルへ結び付ける実体があるとは限りません。

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

1
cat calculate.py
def calculate(a, b):
    return a + b * 2

result = calculate(3, 4)
print(result)
2
python3 compile_demo.py compile
compiled: code
defined: []
3
python3 compile_demo.py run
11
result in namespace: 11
4
python3 compile_demo.py bytecode
bytes: 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 
5
cat evaluation.py python3 evaluation.py
def 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 False
6
head -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;
}
7
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
8
clang main.o -o incomplete 2> link-error.txt status=$? printf 'exit=%s ' "$status" grep -E '"_add"|symbol.s. not found' link-error.txt
exit=1
  "_add", referenced from:
ld: symbol(s) not found for architecture arm64
9
clang main.o add.o -o combined ./combined
7
10
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
11
cat jit.js
function 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"));
12
node --allow-natives-syntax --trace-opt --trace-deopt jit.js > jit.trace python3 summarize_jit.py jit.trace
phase warmup 3 7
compiled add: TURBOFAN_JS
phase number 11
deoptimized add: not a Smi
phase string ab

演習完了