翻譯 之 VMT

Virtual method table(虛擬方法表):

    一個虛擬方法表(VMT)/虛擬功能表/虛擬呼叫表/分發表是使用在編程語言中支持動態分發(或運行時方法綁定)的一種機制。

    無論何時,一個類定義了一個虛擬方法,大多數編譯器向類指定一個指向虛擬方法表(VMT或Vtable)的(虛擬)函數的指針數組添加一個隱藏的成員變量。這些指針在運行時用於適當的函數調用,因爲在編譯時它可能還不知道是否要調用基函數或者是從繼承基類的類實現的派生類。

    假設一個程序在繼承層次結構中包含幾個類:一個父類: Cat,兩個子類: HouseCat, Lion。Cat類定義了一個名叫speak的虛擬函數(方法),因此他的子類可能提供一個適當的實現。(例如:meow或者roar)。

    當程序調用一個Cat引用(可以指代實例Cat,或者HouseCat,Lion實例)上調用speak函數時,這個代碼必須能夠確定調用應該被調用到的函數的實現。這取決於對象的實際類,而不是它聲明的類Cat。這個類通常不能靜態地(即,在編譯時)確定類,所以編譯器也不能決定哪個函數在運行時被調用了。這個調用必須動態地(即,在運行時)被分發到正確的函數。

    實現這種動態分發有很多不同的方法,但是虛擬方法表解決方案在C++及其相關語言(如D和C#)中尤爲常見。將對象的編程接口從實現分離出來的語言,像Visual Basic和 Delphi, 也可以使用虛擬表實現。因爲它允許對象使用不同的實現,只需要使用一組不同的方法指針。

內容

1. Implementation(實現)

2. Example(例子)

3. Multiple inheritance and thunks(多重繼承和指針修復)

4. Invocation(調用)

5. Efficiency(效率)

6. Comparison with alternatives(比較和替代)

7. See also(參見)

8. Notes(筆記)

9. References(參考)

1. Implementation(實現)

    一個對象的調度表將包含對象的動態綁定方法的地址,方法調用是通過從對象的調度表提取方法地址來執行的。調度表對於屬於同一類的所有對象是相同的,因此通常在它們之間共享。屬於類型兼容類的對象(例如繼承層次結構中的兄弟)將有相同佈局的調度表:給定方法的地址將對所有兼容類將以相同的偏量出現。因此,從給定的調度表偏移得到方法的地址將獲得對應於對象的實際類的方法[1]。

    C++規範並沒有要求必須如何實現動態調度,但是編譯器在相同基礎模型上通常使用較小的變化。

    典型地,編譯器爲每個類創建一個單獨的虛擬表。當一個對象被創建時,一個指向這個虛擬表的指針,被稱爲虛擬表指針,vpointer或VPTR,作爲該對象的隱藏成員添加。因此,編譯器必須在每個類的構造函數生成“隱藏”代碼,去初始化一個新對象的虛擬表指針到這個類的虛擬表指針。

    很多編譯器將虛擬表指針作爲對象的最後一個成員,而另外的編譯器將虛擬指針表作爲對象的第一的成員。便攜式源代碼工作方式[2]。例如,g++以前將虛擬表指針放在對象的末尾[3]。


2. Example(例子)

    思考下面C++語法的類聲明:

class B1 {
public:
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual void f2() {}
  int int_in_b2;
};


    使用派生以下類:

class D : public B1, public B2 {
public:
  void d() {}
  void f2() {}  // override B2::f2()
  int int_in_d;
};


    下面是C++代碼片段:

B2 *b2 = new B2();
D  *d  = new D();


    g++ 3.4.6 從GCC爲對象b2產生下面32位存儲佈局[nb 1]:

b2:

  +0: pointer to virtual method table of B2

  +4: value of int_in_b2

virtual method table of B2:

  +0: B2::f2()

    下面是對象d的存儲佈局:

d:

  +0: pointer to virtual method table of D (for B1)

  +4: value of int_in_b1

  +8: pointer to virtual method table of D (for B2)

 +12: value of int_in_b2

 +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):

  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):

  +0: D::f2()   // B2::f2() is overridden by D::f2()

    注意這些函數在聲明(像f0()和d)時並不攜帶關鍵字virtual,一般不會出現在虛擬表中。由默認構造函數構成的是特例。

    在類D中重寫方法f2()是通過複製虛擬方法表B2並將指針B2::f2()替換成D::f2()實現的。


3. Multiple inheritance and thunks(多重繼承和指針修復):

    g++編譯器實現了在類D中使用兩個虛擬方法表對類B1和B2的多重繼承,它們都是類D的基類。(實現多重繼承還有另一種方法,但這是最常用的)。這使指針修復成爲必要。也叫thunks

    思考下面C++ 代碼:

D  *d  = new D();
B1 *b1 = d;
B2 *b2 = d;


    當執行這段代碼後,d和b1指向同一存儲位置。b2將指向d+8的位置(8位超出了d的存儲位置)。因此,b2指向d中的“看起來像”B2的實例的區域,即具有與B2的實例相同的存儲器佈局。


4. Invocation(調用):

    d->f1()調用的處理方式是通過取消d的D::B1的虛擬指針引用,查找虛擬方法表中f1項,然後取消這段調用代碼的引用。

    在單項繼承的例子中(或者是一種語言中的單項繼承),如果虛擬指針始終在d的第一元素(與許多編譯器一樣)這將減少到以下僞C++.

(*((*d)[0]))(d)

    *d是指,虛擬方法表中D和[0]指向虛擬方法表中的第一個方法。參數d成爲了指向這個對象的指針。

    在更一般的情況下,調用 B1::f1() 或 D::f2()更復雜些:

(*(*(d[+0])[0]))(d)  

(*(*(d[+8])[0]))(d+8)

    調用d->f1()是通過將B1指針作爲一個參數。調用d->f2()是通過將B2指針作爲一個參數。這第二個調用需要一個修復(fixup)來產生正確的指針。調用B2::f2是不可能的,因爲在D的實現中它已經被重寫了。B2::f2的位置已經不在D的虛擬表中。通過對比,調用d->f0()更簡單:

(*B1::f0)(d)


5. Efficiency(效率):

    一個虛擬調用要求至少一個額外的索引取消引用,有時候一個“fixup”的增加,與一個非虛擬調用功能相比,這只是簡單的跳轉到編譯指針。因此調用虛擬方法本質上比調用非虛擬方法慢。一個在1996年做的實現表明,大約6~13%的執行時間是花在簡單的調度到正確的方法。雖然開銷可以高達50%[4]。虛擬方法的花費在現代的CPU架構上可能不是那麼高,由於更大的緩存和更好的分支預測。

    進一步說,在JIT編譯沒有使用的環境中,虛擬方法調用通常不能內聯。在某些情況下,編譯器可能會執行稱爲半虛擬化的進程。例如,查找和間接調用被每個內聯體的條件執行替換,但是這種優化並不常見。

    爲了避免這些開銷,編譯器通常在編譯時可以解決的調用時避免使用虛擬表。

    因此,以上對f1的調用可能不需要一個虛擬表查詢,因爲編譯器可能可以告訴d在這個點上只能持有D,並且D不重寫f1。或者編譯器(或者優化器)或許可以檢測到在程序的任何地方沒有B1的子類覆蓋f1。調用B1::f1 或B2::f2可能不會要求一個虛擬表查詢,因爲明確地指定了實現。(雖然它仍需要‘this’指針的fixup)


6. Comparison with alternatives(比較和替代):

    虛擬表與實現動態調度通常是一個很好的性能交易,但是仍然有適用條件,例如二進制樹分發,具有較高的性能但是成本不一[5]。

    然而,虛擬表只允許在特殊參數‘this’上進行單次調度。相比之下,多次調度(例如在CLOS或Dylan)可以在調度時考慮所有參數的類型。虛擬表只有在調度被約束到已知的一組方法時才起作用。所以它們可以放在一個編譯時建立的簡單數組,與鴨式打字語言(如Smalltalk,Python或JavaScript)相反。

    提供這些功能中的一個或兩個的語言通常通過在hash表查找字符串或其他一些等效的方法進行調度。有各種不同的技術可以使他更快(例如,實習/令牌化方法名稱,高速緩存查找,即時編譯)。


7. See also(參見):

    虛擬方法

    虛擬繼承

    分支表


8. Notes(筆記)

    G ++的-fdump-class-hierarchy參數可用於轉儲虛擬方法表以進行手動檢查。 對於AIX VisualAge XlC編譯器,請使用-qdump_class_hierarchy轉儲類層次結構和虛擬功能表佈局。


9. References(參考)

    Margaret A. Ellis and Bjarne Stroustrup (1990) The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley. (ISBN 0-201-51459-1)

    [1]. Ellis & Stroustrup 1990, pp. 227–232

    [2]. Danny Kalev. "C++ Reference Guide: The Object Model II". 2003. Heading "Inheritance and Polymorphism" and "Multiple Inheritance".

    [3]. C++ ABI Closed Issues at the Wayback Machine (archived 25 July 2011)

    [4]. Driesen, Karel and Hölzle, Urs, "The Direct Cost of Virtual Function Calls in C++", OOPSLA 1996

    [5]. Zendra, Olivier and Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java", Pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)

原文來自:https://en.wikipedia.org/wiki/Virtual_method_table#cite_note-4



總結:每個對象的指針的第一個地址指向一張表(虛擬方法表),這個表裏會有各方法實現的地址

具體:

虛擬方法表:

前提:在new一個對象時會先檢查這個對象有沒有父類,如果有父類就會在這個對象的第一個指針地址上創建一張虛擬方法表。

每一個對象地址會有一個指針,這個指針指向一張表(即虛擬方法表),這個表裏會有一個叫call的名字,這個名字會有一個地址,指向這個名字的方法實現。

但是如果子類的方法不是繼承自父類,而是子類自己特有的就不會在虛擬方法表裏存在。

如果子類繼承了父類但是沒有重寫父類的這個方法,這個會記錄在虛擬方法表裏,這個名字指向的地址就是父類這個方法的地址。

public class Callable{
  public void call() {
  System.out.print("say hello");
  }

  public void back(){
  System.out.println("say bye");
  }
}
public class void CallExecutor extends Callable{

  @Override
  public void call() {
  System.out.println("say hello world");
  }

  public void doOther() {
  System.out.printlln("do any other thing...");
  }
}
public class Achieve{
  public void getCall(Callable callable){
  callable.call();
  }
}
public void main(String[] args) {
  Achieve achieve = new Achieve();
  Callable object = new CallExecutor();
  achieve.getCall(object);
}


在這裏,new CallExecutor這個對象時生成一個虛擬方法表

vtable:

VPTR

--------------------------------

call -> 0x00000010-----

public static CallExecutor(CallExecutor this)
{
  public void call()
  {
   System.out.println("say hello world")
  }
}


back -> 0x00000050-----

這個地址就是父類back方法的地址(同一個方法的實現在內存中只有一個方法塊)

public static CallExecutor(CallExecutor this)
{
  public void back(){                    
    System.out.println("say bye"); 
  }
}



--------------------------------


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章