一個物件是什麼#

抽象層面:「資料 + 行為」。VM 層面:「實例變數 + 方法表」。

struct Instance {
    Object  obj;
    Class  *klass;            // 指向自己的類別
    Value  *fields;           // 實例變數陣列
};

struct Class {
    Object   obj;
    char    *name;
    Class   *parent;          // superclass,root 為 NULL
    HashMap *methods;         // method name → Method
    HashMap *constants;
    int      field_count;
};

兩層 indirection:instance ➡️ class ➡️ method。每個 method call 都要走這條鏈。

Method dispatch#

obj.foo(x, y) 的執行:

1. 取出 obj 的 class
2. 在 class.methods 找 "foo"
3. 找不到 → 走 class.parent
4. 還找不到 → method_missing 或 NoMethodError
5. 找到 → 用 obj 當 self、x, y 當引數,呼叫 method

bytecode 概念:

LOAD obj
PUSH x
PUSH y
SEND :foo, 2     ;; method name, num args

SEND 的處理:

case OP_SEND: {
    Symbol *name = read_symbol();
    int nargs = read_byte();
    Value receiver = peek(nargs);
    Method *m = lookup_method(receiver_class(receiver), name);
    if (!m) raise_no_method_error();
    call_method(vm, m, receiver, nargs);
    break;
}

lookup_method 是 method dispatch 的核心。每次 SEND 都做一次 hash lookup ── 這就是動態語言慢的另一大原因。下一章 inline cache 就是來解這個。

繼承的 lookup chain#

class Animal
    def name; "anonymous"; end
end

class Dog < Animal
    def bark; "woof"; end
end

d = Dog.new
d.bark   # 找到 Dog#bark
d.name   # Dog 沒有,往 Animal 找
Method *lookup_method(Class *cls, Symbol *name) {
    while (cls) {
        Method *m = hashmap_get(cls->methods, name);
        if (m) return m;
        cls = cls->parent;
    }
    return NULL;
}

對深的繼承鏈、長 method name ➡️ 多次 hash lookup。每次都付代價。

Method 的內容#

struct Method {
    Object    obj;
    Class    *defined_in;     // 在哪個 class 定義(重要!與下面 super 有關)
    Symbol   *name;
    Function *func;           // 內部還是個 function
    bool      is_native;
};

method 本質上是「綁了 class context 的 function」。call 時:

void call_method(VM *vm, Method *m, Value receiver, int nargs) {
    if (m->is_native) {
        Value result = m->native(vm, receiver, nargs, vm->sp - nargs);
        vm->sp -= nargs + 1;
        push(vm, result);
        return;
    }
    // 一般 bytecode:開新 frame
    Frame *f = push_frame(vm, m->func, nargs);
    f->self = receiver;
}

Frame 多一個 self:method body 裡 self 永遠來自這裡。

super 的處理#

class Dog < Animal
    def bark
        super    # 呼叫 Animal#bark(如果有)
    end
end

super 不能簡單地「呼叫 parent class 的 method」── 它必須從當前 method 所在的 class 的 parent 開始找。如果單純說「receiver 是 Dog 的 instance,找它的 parent」是錯的:當前 method 可能定義在更上層。

實作:method 物件記錄 defined_in,super 從 m->defined_in->parent 開始查找。

bytecode:

SUPER :bark, 0    ;; method name + nargs

handler 從當前 frame 的 method 的 defined_in 開始 lookup。

Metaclass:類別本身也是物件#

「class 是 object」── 但既然是 object,它的 class 是什麼?答案叫 metaclass

class Dog; end

Dog.class       # => Class
Dog.class.class # => Class(自循環)

Smalltalk / Ruby 是「everything is object」的代表。Class 自己也是個 Class,循環指向自身。對 VM 實作意味著:

  • Class 物件本身有 klass 欄位
  • 「class 的 method」不是 instance method ── 它是 metaclass 的 method
struct Class {
    Object   obj;            // obj.klass = metaclass
    Class   *metaclass;
    ...
};

每個 class 自己有獨立 metaclass(singleton class)── 這給 Ruby 帶來 class << self 等語法強大度。

Python / Java 比較簡化:

  • Python:type 是所有 class 的 type,但可以自訂 metaclass
  • Java:class 是 java.lang.Class 的 instance,但不可動態擴展 method

實例變數#

從 instance 角度:

class Dog
    def initialize(name)
        @name = name
    end
end

@name 存在哪?

方案 A:HashMap per instance#

struct Instance {
    Object   obj;
    Class   *klass;
    HashMap *ivars;          // 動態,可加新欄位
};

@name = hash lookup。慢但極彈性。CPython 的 __dict__ 概念。

方案 B:固定 slot 陣列#

class 編譯期決定有哪些 ivar、給每個分配一個 index:

struct Instance {
    Object   obj;
    Class   *klass;
    Value    fields[];       // 對齊到 class 的 ivar 表
};

@name = instance->fields[INDEX_OF_NAME],O(1)。 代價:不能在 runtime 動態加欄位。

方案 C:shape / hidden class(混合)#

V8、JSC、TruffleRuby 都用這個:邏輯上像 hashmap(可動態加),實作上是固定 layout(O(1) 存取)。下一章專門講。

多重繼承 / mixin / interface#

純單繼承(Java、Smalltalk)#

每個 class 一個 parent。簡單但表達力受限。

多重繼承(C++、Python)#

允許多個 parent。lookup 順序成問題:菱形繼承怎麼解?

Python 用 C3 linearization

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

D.__mro__   # [D, B, C, A, object]

VM 在 class 建立時計算 MRO(Method Resolution Order)並存起來,lookup 時走這條線性鏈而非 tree。

Mixin / module(Ruby)#

module Walker
    def walk; "walking"; end
end

class Dog
    include Walker
end

Ruby 把 module 插入 class 的 ancestor chain。lookup 時走 chain:Dog ➡️ Walker ➡️ Object ➡️ …

Interface(Java)#

interface 不帶 method body(早期),實作 class 必須提供。但 Java 8+ 允許 default method。 VM 層面:interface 也是 class 的一種,但 lookup 時要做 invokeinterface 的特殊處理(itable)。

new 與初始化#

d = Dog.new("Rex")

兩個分離的步驟:

  1. allocate:配新 Instance object,連到 class,所有 ivar 初始化為 nil
  2. initialize:呼叫 Dog#initialize("Rex")
case OP_NEW: {
    Class *cls = AS_CLASS(pop());
    int nargs = read_byte();
    Instance *inst = allocate_instance(cls);
    Value receiver = OBJ_VAL(inst);

    // 把 receiver 插到引數下面,呼叫 initialize
    Value *args = vm->sp - nargs;
    memmove(args + 1, args, nargs * sizeof(Value));
    *args = receiver;
    vm->sp++;

    Method *init = lookup_method(cls, sym_initialize);
    if (init) call_method(vm, init, receiver, nargs);
    push(vm, receiver);   // 結果是新 instance
    break;
}

各語言對「沒寫 initialize 怎麼辦」的處理不同。Ruby、Python 給空實作;C++ 自動產生。

protected / private 怎麼實作#

class Foo
    private
    def secret; end
end

f = Foo.new
f.secret    # NoMethodError: private method

實作:method 物件帶 visibility flag。SEND opcode 看 flag:

  • private:必須是 implicit self.method 形式
  • protected:必須由同類或子類的方法呼叫
  • public:任何 caller

JVM bytecode 用 invokespecialinvokevirtual 等不同 opcode 表達 visibility 與 dispatch 規則。

動態增刪方法#

Ruby、Python 允許 runtime 改 class:

class String
    def shout
        upcase + "!"
    end
end

"hi".shout    # "HI!"

實作:直接修改 class 的 method 表。下次 lookup 看到新 method。

代價:

  • inline cache 必須失效
  • JIT 編譯過的 code 必須 deoptimize(執行期假設可能不成立)
  • 安全沙箱要謹慎(避免污染 base class)

開放與封閉的設計權衡#

特性動態(Ruby、Python)靜態(Java、C#)
可動態加 method否(需 reflection trick)
效能(無 JIT)慢(每次 lookup)快(編譯期決定 vtable index)
效能(有 JIT)接近靜態(inline cache)接近原生
工具支援較難分析編譯期可保證

「動態語言比靜態慢」的根源就在這 ── 大部分效能都耗在 method dispatch。

小結#

  • 物件 = instance + class(method table 與 ivar layout)
  • method dispatch:lookup chain,遍歷 class hierarchy
  • super 從當前 method 的定義 class 的 parent 開始找
  • ivar 三種策略:hashmap、固定 slot、shape/hidden class
  • 繼承多樣:單繼承、多重繼承、mixin、interface ── lookup 都是把它們線性化
  • 動態增刪 method 強大但對 cache、JIT 是挑戰

下章看 shape 與 inline cache 怎麼把 dispatch 搞快。