目次
プログラミング言語が「機械語に翻訳されて実行される」ということは知っていても、その具体的な仕組みまで理解している開発者は少ない。コンパイラとインタプリタの違いは単に「速いか遅いか」ではない。翻訳のタイミング、最適化の機会、実行モデル、そしてデバッグ体験に至るまで、すべての側面に影響を与える根本的な設計選択だ。
本記事では、コンパイラとインタプリタの基本原理から、現代のハイブリッド実装(JIT コンパイル)までを体系的に解説する。
コンパイラ vs インタプリタ——根本的な違い
実行モデルの対比
【コンパイル方式( Ahead-of-Time Compilation)】
ソースコード ──→ [コンパイラ] ──→ 機械語 ──→ [CPU] ──→ 実行
(例:C, C++, Rust, Go)
特点:
・実行前の翻訳が必要(ビルドステップ)
・一度コンパイルすれば再翻訳不要
・最適化の機会が多い
・クロスプラットフォーム対応は別途コンパイルが必要
【インタプリタ方式】
ソースコード ──→ [インタプリタ] ──→ 逐次実行
(例:Python, Ruby, JavaScript)
特点:
・翻訳不要、即時実行可能
・実行時に逐次解析(オーバーヘッド)
・プラットフォーム非依存(インタプリタがあればOK)
・動的機能(eval、メタプログラミング)が実装しやすい
処理フローの詳細比較
def simulate_compiler_flow(source_code):
"""コンパイル方式のシミュレーション"""
# 1. 字句解析(Lexical Analysis)
tokens = tokenize(source_code)
# 2. 構文解析(Parsing)
ast = parse(tokens)
# 3. 意味解析(Semantic Analysis)
annotated_ast = analyze_types(ast)
# 4. 最適化(Optimization)
optimized_ast = optimize(annotated_ast)
# 5. コード生成(Code Generation)
machine_code = generate_code(optimized_ast)
# 6. 実行(Execution)
result = execute(machine_code)
return result
def simulate_interpreter_flow(source_code):
"""インタプリタ方式のシミュレーション"""
# 1. 字句解析
tokens = tokenize(source_code)
# 2. 構文解析
ast = parse(tokens)
# 3. 逐次実行(各ノードを直接評価)
result = interpret(ast)
return result
def tokenize(code):
"""字句解析:ソースコードをトークンに分割"""
# 例: "x = 1 + 2" → ['x', '=', '1', '+', '2']
pass
def parse(tokens):
"""構文解析:トークンを抽象構文木(AST)に変換"""
# 例:['x', '=', '1', '+', '2']
# → Assign(Variable('x'), BinOp(1, '+', 2))
pass
def interpret(ast):
"""AST を直接評価して実行"""
if isinstance(ast, Assign):
value = interpret(ast.value)
set_variable(ast.target, value)
elif isinstance(ast, BinOp):
left = interpret(ast.left)
right = interpret(ast.right)
return apply_operator(left, ast.op, right)
# ... 他のノードタイプ
コンパイルプロセス——5つの段階
1. 字句解析(Lexical Analysis)
ソースコードをトークンという意味のある最小単位に分割する。
import re
class Token:
def __init__(self, type_, value):
self.type = type_
self.value = value
def __repr__(self):
return f"Token({self.type}, {self.value!r})"
def lexical_analysis(source_code):
"""字句解析:ソースコードをトークン列に変換"""
token_specifications = [
('NUMBER', r'\d+(\.\d*)?'),
('ASSIGN', r'='),
('PLUS', r'\+'),
('MINUS', r'-'),
('TIMES', r'\*'),
('DIVIDE', r'/'),
('LPAREN', r'\('),
('RPAREN', r'\)'),
('ID', r'[A-Za-z_][A-Za-z0-9_]*'),
('NEWLINE', r'\n'),
('SKIP', r'[ \t]+'),
('MISMATCH', r'.'),
]
tok_regex = '|'.join(f'(?P<{pair[0]}>{pair[1]})'
for pair in token_specifications)
tokens = []
for mo in re.finditer(tok_regex, source_code):
kind = mo.lastgroup
value = mo.group()
if kind == 'SKIP':
continue # 空白はスキップ
elif kind == 'MISMATCH':
raise RuntimeError(f"予期せぬ文字:{value!r}")
else:
tokens.append(Token(kind, value))
return tokens
# 使用例
source = "x = 1 + 2 * 3"
tokens = lexical_analysis(source)
print(tokens)
# 出力:[Token(ID, 'x'), Token(ASSIGN, '='), Token(NUMBER, '1'), ...]
2. 構文解析(Parsing)
トークン列を**抽象構文木(AST)**に変換する。
class ASTNode:
pass
class Number(ASTNode):
def __init__(self, value):
self.value = value
class Variable(ASTNode):
def __init__(self, name):
self.name = name
class BinOp(ASTNode):
def __init__(self, left, op, right):
self.left = left
self.op = op
self.right = right
class Assign(ASTNode):
def __init__(self, target, value):
self.target = target
self.value = value
class Parser:
"""再帰降下法による構文解析"""
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def current_token(self):
if self.pos < len(self.tokens):
return self.tokens[self.pos]
return None
def parse(self):
"""構文解析を実行し AST を返す"""
return self.parse_assignment()
def parse_assignment(self):
"""代入文の解析:x = expr"""
if (self.current_token() and
self.current_token().type == 'ID' and
self.pos + 1 < len(self.tokens) and
self.tokens[self.pos + 1].type == 'ASSIGN'):
target = Variable(self.current_token().value)
self.pos += 2 # ID と = を消費
value = self.parse_expression()
return Assign(target, value)
return self.parse_expression()
def parse_expression(self):
"""式の解析:term (+|-) term"""
left = self.parse_term()
while (self.current_token() and
self.current_token().type in ('PLUS', 'MINUS')):
op = self.current_token().type
self.pos += 1
right = self.parse_term()
left = BinOp(left, op, right)
return left
def parse_term(self):
"""項の解析:factor (*|/) factor"""
left = self.parse_factor()
while (self.current_token() and
self.current_token().type in ('TIMES', 'DIVIDE')):
op = self.current_token().type
self.pos += 1
right = self.parse_factor()
left = BinOp(left, op, right)
return left
def parse_factor(self):
"""因子の解析:NUMBER | ID | (expr)"""
token = self.current_token()
if token.type == 'NUMBER':
self.pos += 1
return Number(float(token.value))
elif token.type == 'ID':
self.pos += 1
return Variable(token.name)
elif token.type == 'LPAREN':
self.pos += 1
expr = self.parse_expression()
self.pos += 1 # RPAREN を消費
return expr
# 使用例
tokens = lexical_analysis("x = 1 + 2 * 3")
parser = Parser(tokens)
ast = parser.parse()
3. 意味解析(Semantic Analysis)
AST が意味的に正しいか検証し、型情報などを付与する。
class SemanticAnalyzer:
"""意味解析:型チェックとシンボルテーブル管理"""
def __init__(self):
self.symbols = {} # 変数の型を記録
def analyze(self, node):
"""AST を走査して型チェック"""
method_name = f'visit_{type(node).__name__}'
visitor = getattr(self, method_name, self.generic_visit)
return visitor(node)
def visit_Assign(self, node):
target_type = self.analyze(node.target)
value_type = self.analyze(node.value)
# 型チェック:代入先の型と値の型が一致するか
if target_type != value_type:
raise TypeError(
f"型エラー:{target_type} に {value_type} は代入できません"
)
return target_type
def visit_Variable(self, node):
if node.name not in self.symbols:
raise NameError(f"未定義変数:{node.name}")
return self.symbols[node.name]
def visit_Number(self, node):
return 'NUMBER'
def visit_BinOp(self, node):
left_type = self.analyze(node.left)
right_type = self.analyze(node.right)
# 演算子の型チェック
if left_type != 'NUMBER' or right_type != 'NUMBER':
raise TypeError("数値演算には数値が必要です")
# 演算子の種類チェック
if node.op == 'DIVIDE' and isinstance(node.right, Number):
if node.right.value == 0:
raise ZeroDivisionError("ゼロ除算はできません")
return 'NUMBER'
def generic_visit(self, node):
raise NotImplementedError(
f"{type(node).__name__} の訪問者が未実装です"
)
# 使用例
analyzer = SemanticAnalyzer()
analyzer.symbols['x'] = 'NUMBER'
analyzer.analyze(ast) # 型エラーがなければ OK
4. 最適化(Optimization)
中間表現を最適化し、より効率的なコードを生成する。
class Optimizer:
"""AST 最適化:定数畳み込み、不要コード削除"""
def optimize(self, node):
method_name = f'visit_{type(node).__name__}'
visitor = getattr(self, method_name, self.generic_visit)
return visitor(node)
def visit_BinOp(self, node):
# 再帰的に最適化
left = self.optimize(node.left)
right = self.optimize(node.right)
# 定数畳み込み(Constant Folding)
if isinstance(left, Number) and isinstance(right, Number):
if node.op == 'PLUS':
return Number(left.value + right.value)
elif node.op == 'MINUS':
return Number(left.value - right.value)
elif node.op == 'TIMES':
return Number(left.value * right.value)
elif node.op == 'DIVIDE' and right.value != 0:
return Number(left.value / right.value)
return BinOp(left, node.op, right)
def visit_Assign(self, node):
value = self.optimize(node.value)
return Assign(node.target, value)
def generic_visit(self, node):
return node
# 最適化の例
# 元の式:x = 1 + 2 * 3
# 最適化後:x = 7(定数畳み込み)
5. コード生成(Code Generation)
最適化された AST を機械語または中間コードに変換する。
class CodeGenerator:
"""仮想マシンのバイトコード生成"""
def __init__(self):
self.code = []
self.variables = {}
def generate(self, node):
method_name = f'emit_{type(node).__name__}'
visitor = getattr(self, method_name, self.generic_visit)
visitor(node)
return self.code
def emit_Assign(self, node):
# 値を先に評価
self.generate(node.value)
# 変数のインデックスを割り当て
if node.target.name not in self.variables:
self.variables[node.target.name] = len(self.variables)
var_index = self.variables[node.target.name]
self.code.append(('STORE', var_index))
def emit_Number(self, node):
self.code.append(('PUSH', node.value))
def emit_Variable(self, node):
var_index = self.variables[node.name]
self.code.append(('LOAD', var_index))
def emit_BinOp(self, node):
self.generate(node.left)
self.generate(node.right)
op_map = {
'PLUS': 'ADD',
'MINUS': 'SUB',
'TIMES': 'MUL',
'DIVIDE': 'DIV'
}
self.code.append((op_map[node.op],))
def generic_visit(self, node):
raise NotImplementedError(f"{type(node).__name__} の生成が未実装")
# 使用例
# x = 1 + 2 * 3 の場合:
# PUSH 1
# PUSH 2
# PUSH 3
# MUL (2 * 3 = 6)
# ADD (1 + 6 = 7)
# STORE 0 (変数 x に保存)
インタプリタの実装方式
木解釈(Tree-walk Interpreter)
AST を直接辿って実行する最もシンプルな方式。
class TreeWalkInterpreter:
"""AST を直接解釈して実行"""
def __init__(self):
self.environment = {} # 変数の値を格納
def interpret(self, node):
method_name = f'execute_{type(node).__name__}'
visitor = getattr(self, method_name, self.generic_execute)
return visitor(node)
def execute_Number(self, node):
return node.value
def execute_Variable(self, node):
if node.name not in self.environment:
raise NameError(f"未定義変数:{node.name}")
return self.environment[node.name]
def execute_BinOp(self, node):
left = self.interpret(node.left)
right = self.interpret(node.right)
if node.op == 'PLUS':
return left + right
elif node.op == 'MINUS':
return left - right
elif node.op == 'TIMES':
return left * right
elif node.op == 'DIVIDE':
if right == 0:
raise ZeroDivisionError("ゼロ除算")
return left / right
def execute_Assign(self, node):
value = self.interpret(node.value)
self.environment[node.target.name] = value
return value
def generic_execute(self, node):
raise NotImplementedError(f"{type(node).__name__} の実行が未実装")
# 使用例
interpreter = TreeWalkInterpreter()
interpreter.interpret(ast)
print(interpreter.environment) # {'x': 7.0}
バイトコード解釈(Bytecode Interpreter)
中間コード(バイトコード)を仮想マシンで実行する方式。
class BytecodeVM:
"""スタックベースの仮想マシン"""
def __init__(self):
self.stack = []
self.variables = {}
def run(self, bytecode):
"""バイトコードを実行"""
for instruction in bytecode:
op = instruction[0]
if op == 'PUSH':
self.stack.append(instruction[1])
elif op == 'LOAD':
var_index = instruction[1]
value = list(self.variables.values())[var_index]
self.stack.append(value)
elif op == 'STORE':
var_index = instruction[1]
value = self.stack.pop()
var_name = list(self.variables.keys())[var_index] \
if var_index < len(self.variables) \
else f"var_{var_index}"
self.variables[var_name] = value
elif op == 'ADD':
right = self.stack.pop()
left = self.stack.pop()
self.stack.append(left + right)
elif op == 'SUB':
right = self.stack.pop()
left = self.stack.pop()
self.stack.append(left - right)
elif op == 'MUL':
right = self.stack.pop()
left = self.stack.pop()
self.stack.append(left * right)
elif op == 'DIV':
right = self.stack.pop()
left = self.stack.pop()
if right == 0:
raise ZeroDivisionError()
self.stack.append(left / right)
return self.stack[-1] if self.stack else None
# 使用例
bytecode = [
('PUSH', 1),
('PUSH', 2),
('PUSH', 3),
('MUL',),
('ADD',),
('STORE', 0)
]
vm = BytecodeVM()
result = vm.run(bytecode)
print(f"結果:{result}") # 7.0
現代のハイブリッド方式——JIT コンパイル
JIT(Just-In-Time)コンパイルの仕組み
【JIT コンパイルのフロー】
ソースコード ──→ [コンパイラ] ──→ バイトコード ──→ [インタプリタ] ──→ 実行
(事前) (実行時)
│
│ 頻繁に実行される関数を検出
▼
[JIT コンパイラ] ──→ 機械語 ──→ 高速実行
(実行時)
プロファイリングに基づく最適化
class JITCompiler:
"""簡易 JIT コンパイラ(概念実装)"""
def __init__(self):
self.call_counts = {} # 関数の実行回数を記録
self.compiled_functions = {} # コンパイル済み関数
self.interpreter = TreeWalkInterpreter()
self.threshold = 100 # JIT コンパイルの閾値
def execute_function(self, func_name, func_ast, args):
"""関数を実行(必要に応じて JIT コンパイル)"""
# 実行回数をカウント
if func_name not in self.call_counts:
self.call_counts[func_name] = 0
self.call_counts[func_name] += 1
# 閾値を超えたら JIT コンパイル
if (self.call_counts[func_name] >= self.threshold and
func_name not in self.compiled_functions):
self.compile_function(func_name, func_ast)
# コンパイル済みなら機械語で実行
if func_name in self.compiled_functions:
return self.compiled_functions[func_name](*args)
# それ以外はインタプリタで実行
return self.interpreter.interpret(func_ast)
def compile_function(self, func_name, func_ast):
"""関数を JIT コンパイル(実際には LLVM 等を使用)"""
print(f"JIT コンパイル:{func_name}")
# 実際の JIT コンパイルは複雑(ここでは擬似コード)
self.compiled_functions[func_name] = self.generate_machine_code(func_ast)
def generate_machine_code(self, func_ast):
"""機械語を生成(概念実装)"""
# 実際には LLVM、V8 の TurboFan などが複雑な最適化を実行
def compiled(*args):
# 最適化された実行
return sum(args) # 簡略化
return compiled
# 使用例
jit = JITCompiler()
# 100 回未満はインタプリタ
for i in range(50):
jit.execute_function('fib', fib_ast, (10,))
# 100 回超で JIT コンパイル発生
for i in range(100):
jit.execute_function('fib', fib_ast, (20,))
# 出力:JIT コンパイル:fib
主要な JIT 実装
| 実装 | 言語 | JIT 方式 | 特徴 |
|---|---|---|---|
| V8(TurboFan) | JavaScript | メソッド JIT | インラインキャッシング、脱最適化 |
| JVM(HotSpot) | Java | メソッド JIT | レイヤードコンパイル、プロファイリング |
| CLR | C# | メソッド JIT | 汎用型サポート、ジェネリクス |
| PyPy | Python | トレーシング JIT | トレースの記録と再生 |
| LuaJIT | Lua | トレーシング JIT | 世界最速級の JIT 実装 |
最適化手法の実際
1. インライン展開(Inlining)
# 元のコード
def add(a, b):
return a + b
def calculate(x, y):
return add(x, y) * 2
# インライン展開後
def calculate(x, y):
return (x + y) * 2 # 関数呼び出しのオーバーヘッド削除
2. ループ不変式コード移動(Loop Invariant Code Motion)
# 元のコード
for i in range(1000):
result = x * 100 + i # x * 100 は毎回同じ
# 最適化後
temp = x * 100 # ループ外に移動
for i in range(1000):
result = temp + i
3. 共通部分式削除(Common Subexpression Elimination)
# 元のコード
a = x * y + z
b = x * y + w
# 最適化後
temp = x * y # 共通部分を 1 回だけ計算
a = temp + z
b = temp + w
実行速度の比較——定量的評価
import time
def benchmark_execution(n_iterations, execution_type):
"""実行方式ごとのベンチマーク"""
code = "x = 1 + 2 * 3"
if execution_type == 'compiled':
# ネイティブコンパイル(C 等)
# 実際には別プロセスで実行
overhead = 0.00001 # 10μs
elif execution_type == 'jit':
# JIT コンパイル(V8 等)
overhead = 0.0001 # 100μs
elif execution_type == 'bytecode':
# バイトコード解釈
overhead = 0.001 # 1ms
else: # tree-walk
# 木解釈
overhead = 0.01 # 10ms
# 実際の処理時間(簡易モデル)
base_time = 0.000001 # 1μs
total_time = base_time * n_iterations + overhead
return total_time
# ベンチマーク
iterations = 10000
results = {}
for exec_type in ['compiled', 'jit', 'bytecode', 'tree-walk']:
time_taken = benchmark_execution(iterations, exec_type)
results[exec_type] = time_taken
print(f"{exec_type}: {time_taken*1000:.2f}ms")
# 相対速度
base = results['compiled']
for exec_type, time_taken in results.items():
speedup = base / time_taken
print(f"{exec_type}: {speedup:.2f}x")
典型的な結果:
- ネイティブコンパイル:1x(基準)
- JIT コンパイル:2-5x
- バイトコード解釈:10-50x
- 木解釈:50-100x
各言語の実装戦略
C/C++/Rust—— Ahead-of-Time コンパイル
特徴:
・完全な静的コンパイル
・リンク時に最適化(LTO)
・実行ファイル単独で動作
・クロスプラットフォームはクロスコンパイル
Java——バイトコード + JIT
特徴:
・一度バイトコードにコンパイル
・JVM 上で実行
・JIT がホットスポットを最適化
・プラットフォーム非依存(JVM があれば OK)
Python——バイトコード解釈 + オプション JIT
特徴:
・.pyc ファイルにバイトコード
・CPython はバイトコード解釈
・PyPy は JIT で高速化
・Cython は C に変換
JavaScript——マルチステージ JIT
特徴:
・ソースコードを直接 JIT
・V8: Ignition(インタプリタ)+ TurboFan(JIT)
・初期はインタプリタで高速起動
・ホット関数は JIT で最適化
コンパイラの作成——実用的なツール
ANTLR4(Another Tool for Language Recognition)
// 電卓言語の文法例
grammar Calculator;
expr
: '(' expr ')' # ParensExpr
| expr ('*' | '/') expr # MulDivExpr
| expr ('+' | '-') expr # AddSubExpr
| NUMBER # NumberExpr
;
NUMBER : [0-9]+ ('.' [0-9]+)? ;
WS : [ \t\r\n]+ -> skip ;
LLVM(Low Level Virtual Machine)
# LLVM IR の例(x = 1 + 2 * 3)
from llvmlite import ir
# 型定義
int_type = ir.IntType(32)
# 関数定義
func_type = ir.FunctionType(int_type, [])
func = ir.Function(ir.Module(), func_type, 'main')
# ブロック
block = func.append_basic_block('entry')
builder = ir.IRBuilder(block)
# 定数
const1 = ir.Constant(int_type, 1)
const2 = ir.Constant(int_type, 2)
const3 = ir.Constant(int_type, 3)
# 計算:1 + 2 * 3
mul = builder.mul(const2, const3)
result = builder.add(const1, mul)
builder.ret(result)
まとめ
コンパイラ・インタプリタの核心:
- 根本的な違い: コンパイルは事前翻訳、インタプリタは逐次実行
- コンパイルプロセス: 字句解析 → 構文解析 → 意味解析 → 最適化 → コード生成
- インタプリタ方式: 木解釈(シンプル)とバイトコード(効率的)
- 現代のハイブリッド: JIT コンパイルで両者の利点を統合
- 最適化手法: インライン展開、ループ不変式移動、共通部分式削除
- 実装戦略: 言語ごとに異なる選択(C=純粋コンパイル、Java=バイトコード+JIT)
コンパイラとインタプリタの理解は、単なる知識ではない。コードの実行コストを予測し、パフォーマンスの問題を特定し、言語の制約を把握するための基礎となる。
特に JIT コンパイルの理解は現代的だ。「なぜ初期実行は遅いが、暖機後は速くなるのか」「なぜベンチマークは暖機ランが必要か」——これらすべてがコンパイル戦略に根ざしている。
重要なのは、実行モデルのトレードオフを理解し、適切な言語選択と最適化のアプローチを取ることだ。
参考資料
- “Crafting Interpreters” Robert Nystrom 著(オンライン公開)
- “Engineering a Compiler” Keith Cooper, Linda Torczon 著
- “Compilers: Principles, Techniques, and Tools” Aho, Lam, Sethi, Ullman 著(通称:Dragon Book)
- LLVM Project: https://llvm.org/
- ANTLR4: https://www.antlr.org/
- V8 Developer Guide: https://v8.dev/docs
- PyPy Documentation: https://doc.pypy.org/
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。