目次

プログラミング言語が「機械語に翻訳されて実行される」ということは知っていても、その具体的な仕組みまで理解している開発者は少ない。コンパイラとインタプリタの違いは単に「速いか遅いか」ではない。翻訳のタイミング、最適化の機会、実行モデル、そしてデバッグ体験に至るまで、すべての側面に影響を与える根本的な設計選択だ。

本記事では、コンパイラとインタプリタの基本原理から、現代のハイブリッド実装(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レイヤードコンパイル、プロファイリング
CLRC#メソッド JIT汎用型サポート、ジェネリクス
PyPyPythonトレーシング JITトレースの記録と再生
LuaJITLuaトレーシング 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)

まとめ

コンパイラ・インタプリタの核心:

  1. 根本的な違い: コンパイルは事前翻訳、インタプリタは逐次実行
  2. コンパイルプロセス: 字句解析 → 構文解析 → 意味解析 → 最適化 → コード生成
  3. インタプリタ方式: 木解釈(シンプル)とバイトコード(効率的)
  4. 現代のハイブリッド: JIT コンパイルで両者の利点を統合
  5. 最適化手法: インライン展開、ループ不変式移動、共通部分式削除
  6. 実装戦略: 言語ごとに異なる選択(C=純粋コンパイル、Java=バイトコード+JIT)

コンパイラとインタプリタの理解は、単なる知識ではない。コードの実行コストを予測し、パフォーマンスの問題を特定し、言語の制約を把握するための基礎となる。

特に JIT コンパイルの理解は現代的だ。「なぜ初期実行は遅いが、暖機後は速くなるのか」「なぜベンチマークは暖機ランが必要か」——これらすべてがコンパイル戦略に根ざしている。

重要なのは、実行モデルのトレードオフを理解し、適切な言語選択と最適化のアプローチを取ることだ


参考資料

免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。