android 集成同一interface不同泛型_Dig101:Go之读懂interface的底层设计
Dig101: dig more, simplified more and know more
今天我們聊聊萬物皆可為的接口(interface)底層設計。
interface 被定義為一組方法的簽名。
有了它,我們可以訂立方法契約,去抽象和約束實現。
而 Go 的基礎類型,可以認為是沒有實現任何方法的空 interface,也就是萬物皆為的 interface。
(Go 語言沒有泛型,接口可以作為一種替代實現)
接口也被寄予厚望,主力開發 Russ Cox 曾說過:
從語言設計的角度來看,Go 的接口是靜態的,在編譯時檢查過的,在需要時是動態的。如果我可以將 Go 的一個特性導出到其他語言中,那就是接口。Go Data Structures: Interfaces[1]
那到底 interface 是怎么設計的底層結構呢?
又怎么支持的duck typing[2]?
在類型斷言時又發生了什么?
帶著這些問題,我們往下看
文章目錄
0x01 底層結構一樣么
eface
iface
0x02 類型如何相互轉換
convXXX 的命名
起初的 convT2{I,E} 和 convI2I
針對類型優化后的 convXXX
0x03 類型斷言如何實現
查表是否匹配
嘗試插入更新
動態判定效率優化
0x01 底層結構一樣么
我們知道定義接口有這兩種方式,那他們底層結構是一樣的么?
// 方式1var a interface{}
// 方式2
type Stringer interface {
String() string
}
var b Stringer
答案是【不一樣】
我們用 gdb 打印下對應類型(gdb 相關見 Tips-如何優雅的使用GDB調試Go)
// 空接口類型// 有函數定義的接口類型// itable相關類型以此可見 Go 內部定義了兩種 interface(但都是兩個機器字)
eface
空接口,指沒有定義方法的接口
內部存儲了構造類型(concrete type)type和data
efaceiface
有方法的接口
有了相比eface的type更豐富的itab字段,其中記錄了構造類型及所實現的 interface 類型的類型和方法
iface0x02 類型如何相互轉換
如下代碼,當我們做接口賦值時,Go 又會怎樣填充底層結構呢?
type Binary uint64func (i Binary) String() string {
return strconv.Itoa(int(i))
}
func conversion() {
var b Stringer
var i Binary = 1
b = i // <= 這里發生了什么
println(b.String())
}
gdb 進到 b = i 這一步,會發現他調用了runtime/iface.go:convT64方法實現 iface 的賦值
查閱源碼,會發現很多convXXX函數, 他們是干什么的?
convXXX 的命名
convFrom2To 指代 To=From 的轉換
From 和 To 的類型有三種:(參見cmd/compile/internal/types/type.go:Tie)
- E (eface)
- I (iface)
- T (Type)
這一堆函數看的人眼暈,但參照提交specialize convT2x, don't alloc for zero vals[3]深入分析,就會清晰許多
起初的 convT2{I,E} 和 convI2I
最初只有 convT2{I,E} 和 convI2I
主要實現分配內存(newobject),然后拷貝賦值(typedmemmove)
convI2I 還會有getitab, 具體是什么我們后邊類型斷言時說
然后也在調用他們前(walkexpr)做了優化
- 減少值拷貝
ToType 為類指針(pointer-shaped)或者一個機器字內(int)的話,可以直接存入 interface 的 data 字段(主要優化在這里)
pointer-shaped類型: ptr, chan, map, func, unsafe.Pointer
再輔以 type 的存儲,就只是兩個字(two-word)的拷貝
- 減少內存分配
零值,bool/byte 可以不用分配內存,而用已存在值(zerobase,staticbytes)
只讀的全局變量(readonly global)直接可以用
1kb 以內,不escape到堆上,非interface的變量可以使用棧上分配的臨時變量(stack temporary initialized)
這類 value 最后以取地址形式轉化為 interface:{type/itab, &value}.
- interface 轉空接口(eface)
可以丟棄除type以外的itab
tmp = i.itabif tmp != nil {
tmp = tmp.type
}
e = iface{tmp, i.data}
針對類型優化后的 convXXX
但這里會有一些可以優化的點,如:
- 分配內存是否可以需要清零?
類指針的類型需要清零,不然內存可能有臟數據
但無指針類型(pointer-free)如拷貝時直接可以覆蓋對應內存則不需要
如int其拷貝在一個機器字內完成,不需要分配時清零 (32 位系統上不調用convT64,就可以保證訪問內存是安全的原子操作)
- 是否可以簡化值拷貝?
int,string,slice這些 Type 分配的x拷貝val時,可以簡化為 *(*Type)(x) = val
- 拷貝內存是否可以不增加 gc 調用(寫屏障)?
按 ToType 類型是否含指針區分 類指針類型(pointer-shaped): convT2{E,I} 需要拷貝時 gc 調用(typedmemmove)
無指針類型(pointer-free): convT2{E,I}noptr 不需要拷貝時 gc 調用(memmove)
這樣一看就明白這些函數的用意了,還是為了針對性的提高轉化效率
最后結合其調用處convXXX列表如下:
// cmd/compile/internal/gc/walk.go:walkexprcase OCONVIFACE:
...
fnname, needsaddr := convFuncName(fromType, toType)
| convI2I | iface | 否 |
| convT{16, 32,64} | 整型數據 (無指針, 機器字內) | 否 |
| convTstring | string | 否 |
| convTslice | slice | 否 |
| convT2E | Type | 是 |
| convT2Enoptr | 無指針 Type | 是 |
| convT2I | Type | 是 |
| convT2Inoptr | 無指針 Type | 是 |
不會存在 convE2E 和 convE2I?
needsaddr: 類型不含指針,大小大于 64 位字或未知大小時,使用值的地址來存
0x03 類型斷言如何實現
interface 支持類型斷言,來動態判斷其構造類型,
判定成功可返回對應構造類型,便于調用其方法
可構造類型實現 interface 不需要顯示聲明,
那如下代碼是怎么確定 interface b(構造類型是Binary)實現Stringer呢?
type Binary uint64func (i Binary) String() string {
return fmt.Sprint(i)
}
func typeAssert() {
var b interface{} = Binary(1)
v, ok := b.(Stringer)
println(v, ok)
}
調試后會發現,其調用了assertE2I2
這里函數命名有兩類,如下
assertE2I: v := eface1.(iface1)
assertE2I2: v,ok := eface1.(iface1)
這里有一點,類型斷言非 v,ok 方式的,斷言失敗會 panic)
原來其內部進行了itab表(itabTable)查詢 interface 和構造類型的映射表,如果匹配則說明實現
下邊代碼分析如下
首先初始 512 個 entry 的表
const itabInitSize = 512type itabTableType struct {
// 上限
size uintptr
// 當前用量
count uintptr
entries [itabInitSize]*itab
}
查表是否匹配
在類型斷言中調用 getitab(inter, typ, canfail) 查表
- 先不加鎖 atomic 讀取 itabTable,找到返回
- 未找到加鎖再查一遍,找到返回
- 還沒有就創建一個 itab 添加到表中,添加完后解鎖
- 期間如果判定不匹配則按是否可以 panic(canfail)返回
其中查表用到 itabTable.find(inter, typ),
插入用到 itabAdd(m)
嘗試插入更新
- 插入前需先用m.inter/m._type pair 初始化 m.fun 數組,不匹配則m.fun[0]==0
(m.fun 類型 [1]uintptr,實際指向是大小為接口定義方法數的方法數組。詳見 func (m *itab) init())
用量 count 超過上限的 75%觸發擴容,大小為 2 倍以上(要向上內存對齊),擴容后更新 itabTable 是原子操作
以 itab m 的 interface 類型和構造類型的 hash 計算對應 itabTable 的起始偏移,然后插入到其后第一個不為空的 entry。如果已存在則直接返回
這里用到了開放地址探測法,公式是:
h(i) = h0 + i*(i+1)/2 mod 2^k
具體插入用到 itabTable.add(m)
這里和其實 map 插入的邏輯很相似
動態判定效率優化
不過,這里有一個問題?
假定,interface 定義了ni個方法,構造類型實現nt個方法,
常規匹配構造類型是否實現全部ni個方法需要兩層遍歷,復雜度為O(ni*nt)
這樣在初始化itab.fun或類型斷言匹配時效率會比較低。
Go 設計時也考慮了這個問題,把復雜度降低為O(ni+nt)
這也是使用 hashtable 的原因之一:
首先 interface 的函數定義列表itab.inter.mhdr和構造類型的函數列表itab.fun都是按函數名排好序的
這樣第一次 itab 初始化時,判定構造類型是否實現函數列表可以O(ni+nt)內遍歷完成
然后用開放地址探測法更新到 itabtable 中,查詢時也可以用同樣的方式定位到此 itab 是否存在。
兩個(有序)列表的遍歷匹配代碼精簡如下:
// runtime/iface.go:init()j:=0
imethods:
// 遍歷interface定義函數列表
for k := 0; k < ni; k++ {
// 遍歷構造類型函數列表
for ; j < nt; j++ {
// 如果兩者類型(type),包路徑(pkgpath),函數名(name)匹配
if xxx {
// 將方法記錄到fun0(最終全匹配則賦值給 m.fun)
continue imethods
}
}
// 未全匹配
m.fun[0] = 0
}
m.fun[0] = uintptr(fun0)
總結一下 interface 的底層設計:
- interface 分為空接口(eface)和接口(iface)兩類,但都是兩機器字(two-word)存儲結構
- interface 轉換中針對不同類型做了優化,主要集中于提升內存分配和值拷貝效率
- interface 類型斷言時動態判定,利用有序列表遍歷+全局哈希表表緩存優化判定效率
See More:官方解釋?InterfaceSlice[4]?為什么不能直接轉化
最后留個問題:
下邊這段轉換代碼內部沒有調convT64,為什么?
var b Stringer = Binary(1)_ = b.String()
這個問題下一篇文章再來給出解答。
本文代碼見 NewbMiao/Dig101-Go[5]
參考資料
[1]Go Data Structures: Interfaces: https://research.swtch.com/interfaces
[2]duck typing: https://en.wikipedia.org/wiki/Duck_typing
[3]specialize convT2x, don't alloc for zero vals: https://go-review.googlesource.com/c/go/+/36476
[4]InterfaceSlice: https://github.com/golang/go/wiki/InterfaceSlice
[5]NewbMiao/Dig101-Go: https://github.com/NewbMiao/Dig101-Go/blob/master/types/interface/interface.go
推薦閱讀
- Dig101: Go之for-range排坑指南
- Dig101: Go之靈活的slice
- Dig101: Go之string那些事
- Dig101:Go之讀懂map的底層設計
- Dig101:Go之聊聊struct的內存對齊
- Tips-如何優雅的使用GDB調試Go
原創不易,如果有用,歡迎轉發、點個在看,分享給更多人。
微信內外鏈不能跳轉,戳閱讀原文查看原文中參考資料
????歡迎關注我,一位愛折騰的開發(奶爸):熱衷搬磚、價投。
總結
以上是生活随笔為你收集整理的android 集成同一interface不同泛型_Dig101:Go之读懂interface的底层设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP8 的 JIT 是什么?
- 下一篇: 线程池参数详解_java中常见的六种线程