Go Internals: Go 反射 vs Java 泛型 vs cpp 模板

hitzhangjie發表於2020-03-04

0 簡述

Go語言並不支援範型程式設計(某些內建函式是支援範型的,但是使用者自定義函式不支援範型),但是可以藉助reflect來一定程度上彌補這部分能力的缺失,因為要靠執行時計算所以有執行時開銷,效能比不上真正的範型實現。

Java支援真正的“範型”,泛型程式設計的好處是,編譯時對型別安全性進行檢查,並且模板引數可以是任意型別不用做型別轉換,既安全又方便。由於是在編譯時進行型別檢查,並且Java編譯器會對類、方法、變數級別的模板引數進行型別擦除(Type Erasure,簡單理解就是將模板引數替換成Object型別或者第一個Bound的型別),無執行時開銷,比Go藉助反射模擬範型效能好,也不用像C++一樣拷貝程式碼引起編譯速度下降或者程式碼尺寸膨脹。點選檢視:Java-Type-Erasure

C++通過“模板”來支援“範型”程式設計,之所以加引號,是因為C++不是真正的支援範型程式設計,模板特例化時編譯器其實是生成了一個新類的程式碼。C++模板是通過Macro來進行處理的,相當於複製、貼上了類别範本的程式碼,並替換了模板引數型別。簡言之,就是一個高階的Macro處理系統。但是因為拷貝了程式碼,程式碼膨脹導致了編譯速度下降、檔案尺寸增加。

網上有很多相關的討論,這裡舉個示例簡單總結一下。

1 C++ 類别範本

#include <iostream>
using namespace std;

template <typename T>
class Calc{
    T t1;
    T t2;
public:
    T Add(T t1, T t2) {
        return t1 + t2;
    }
};

int main() {
    Calc<int> calc_int;
    auto sum_int = calc_int.Add(1, 2);
    cout << "1 + 2 = " << sum_int << endl;

    Calc<float> calc_flt;
    auto sum_flt = calc_flt.Add(1.1, 2.2);
    cout << "1.1 + 2.2 = " << sum_flt << endl;

    return 0;
}

這裡其實是建立了兩個不同的類,objdump -dS可以很清晰地看到至少建立了兩個不同的方法Add(T, T),可能會有人認為這是函式過載中的name mangling,其實不是,確實是生成了兩個不同的型別,這個可以通過DWARF相關資訊看出來,首先g++ -s main.cpp得到彙編後檔案main.s,然後檢視該檔案內容並搜尋Calc,下面兩個分別表示Calc<float>模板例項以及Calc<int>模板例項,二者確實屬於兩個不同的型別,一個是用Ltypes95來標識,一個是用Ltypes47來標識。

go反射 vs java泛型 vs cpp模板

2 Java範型

Java中範型的實現依賴於Java中的類繼承機制、型別擦除、型別轉換來實現,最終只會有一個類的示例。

編譯時,編譯器會對模板引數T進行型別擦除,這裡有兩種處理的情形:

  • 模板引數T,沒有繫結一個型別(如T extends Comparable),那麼型別擦除後,模板引數T會用Object進行替代,同時生成對應的型別轉換的程式碼;
  • 模板引數T,有限制型別(如T extends Comparable限定了模板實參必須實現Comparable介面),那麼型別擦除後,模板引數T就用這第一個bound的型別Comparable代替,同時生成對應的型別轉換程式碼。

需要注意的是,編譯時型別擦除雖然會對原始碼做一定的調整,某些資訊看似丟失了,比如List<String> lst被擦除後變為了List<Object> lst,在執行時我們依然可以通過反射機制來獲取lst的元素型別為String,則是為什麼呢?這是因為型別擦除並不是刪除所有型別資訊,模板實參的資訊會以某種形式儲存下來,以便反射時使用。

// 型別擦除前程式碼
List<String> lst = new ArrayList();
lst.Add("hello");
lst.Add("world");
Iterator it = lst.iterator();
for ; it.hasNext(); {
    String el = it.Next();
}

// 型別擦除後程式碼
List lst = new ArrayList();            // 模板實參String,擦除為Object
lst.Add("hello");                    // hello為String,IS-A Object關係成立
lst.Add("world");                   // ...
Iterator it = lst.iterator();       
for ; it.hasNext(); {
    String el = (String) it.Next(); // 編譯器自動插入型別轉換的程式碼
}

由此可見,Java的範型實現,既不會像C++那樣多建立類導致程式碼體積膨脹,也不會帶來執行時開銷,也沒有破壞反射依賴的資訊。

3 go反射

Go1不支援範型,但是它可以結合interface{}以及reflection來模擬範型。反射的效能大約有幾百ns級別的效能損耗,和範型實現比,還是存在一定的效能差距。

Go2已經中已經計劃支援泛型了,拭目以待。

對“泛型”這個寬泛的術語,對比了c++、java、go的一些支援和實現上的差異。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

hitzhangjie

相關文章