這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文地址:http://goworldgs.com/?p=37
在C語言中有一個經典的宏定義,可以將結構體struct內部的某個成員的指標轉化為結構體自身的指標。下面是一個例子,通過FIELD_OFFSET宏計算結構體內一個欄位的位移,函數getT可以從一個F*的指標獲得對應的T*對象。
struct F { int c; int d;} struct T{ int a; int b; struct F f;} #define FIELD_OFFSET(type, field) ((int)(unsigned char *)(((struct type *)0)->field)) struct T* getT(struct F* f) { return (T*)((unsigned char *)f - FIELD_OFFSET(T, F))}
在Golang中能否實現同樣的功能?嘗試寫如下的代碼:
type T struct {a intb intf F}type F struct {c intd int}func (m *F) T1() *T {var dummy *TfieldOffset := uintptr(unsafe.Pointer(&dummy.f)) - uintptr(unsafe.Pointer(dummy))return (*T)(unsafe.Pointer((uintptr)(unsafe.Pointer(m)) - fieldOffset))}
編譯通過,運行!panic: runtime error: invalid memory address or nil pointer dereference。這裡dummy *T是nil,雖然代碼並不訪問dummy所指向的內容,但是Golang依然不允許這樣使用這個指標。
既然Golang不允許使用nil指標,那麼我們可以通過建立一個無用的T對象來繞開這個問題,代碼如下:
func (m *F) T2() *T {var dummy TfieldOffset := uintptr(unsafe.Pointer(&dummy.f)) - uintptr(unsafe.Pointer(&dummy))return (*T)(unsafe.Pointer((uintptr)(unsafe.Pointer(m)) - fieldOffset))}
測試證明這個代碼可以正常工作,並且我們可以使用另外一個函數TBad來進行效能對比:
func (m *F) TBad() *T {return (*T)(unsafe.Pointer(uintptr(unsafe.Pointer(m)) - 16))} func BenchmarkGetPtrByMemberPtr_T2(b *testing.B) {var t Tfor i := 0; i < b.N; i++ {if &t != t.f.T2() {b.Fatal("wrong")}}} func BenchmarkGetPtrByMemberPtr_TBad(b *testing.B) {var t Tfor i := 0; i < b.N; i++ {if &t != t.f.TBad() {b.Fatal("wrong")}}}
測試結果:T2和TBad的運行開銷分別為:1.44 ns/op和0.85 ns/op。
考慮到T2為什麼會比TBad有更大的開銷,我們懷疑T2裡每次都需要在heap上建立一個T對象。如果T對象的大小很大的時候,建立T對象的開銷也會增大,我們可以通過增大結構體T的大小來進行驗證。我們將T結構體的定義修改為:
type T struct {a intb intf Fe [1024]byte}
再次運行發現T2的開銷增大到37.8 ns/op。那麼如何才能消除T結構體大小對這個函數的影響?Golang不允許我們使用nil指標,是不是我們只需要偽造一個*T的非nil指標即可?嘗試寫如下代碼並進行測試:
func (m *F) T3() *T {var x struct{}dummy := (*T)(unsafe.Pointer(&x))fieldOffset := uintptr(unsafe.Pointer(&dummy.f)) - uintptr(unsafe.Pointer(dummy))return (*T)(unsafe.Pointer((uintptr)(unsafe.Pointer(m)) - fieldOffset))}
T3的開銷降低到1.14 ns/op,接近最快的TBad的0.85 ns/op。更進一步的,我們可以直接使用*F指標作為dummy,代碼如下:
func (m *F) T4() *T {dummy := (*T)(unsafe.Pointer(m))fieldOffset := uintptr(unsafe.Pointer(&dummy.f)) - uintptr(unsafe.Pointer(dummy))return (*T)(unsafe.Pointer((uintptr)(unsafe.Pointer(m)) - fieldOffset))}
但是測試表明T4和T3的開銷完全一樣,都是1.14 ns/op。
從目前為止,T3和T4的實現效能非常好,只比TBad裡高一點點。推測原因是TBad不需要計算F類型field的位移,在C語言裡FIELD_OFFSET宏也是在編譯時間進行計算,但是在T3和T4中需要計算一次f *F欄位在T結構體中的位移。我們可以使用一個全域變數來儲存欄位的位移,這樣就不需要每次都進行計算,代碼如下:
var fieldOffset uintptr func init() {dummy := (*T)(unsafe.Pointer(&fieldOffset))fieldOffset = uintptr(unsafe.Pointer(&dummy.f)) - uintptr(unsafe.Pointer(dummy))}func (m *F) T5() *T {return (*T)(unsafe.Pointer((uintptr)(unsafe.Pointer(m)) - fieldOffset))}
測試表明T5的開銷和TBad一樣,都是0.85 ns/op,這個應該已經是極限了。
由於Go語言沒有提供泛型機制,所以每個需要用到這個功能的類都需要定義自己的轉換函式,而不能像C/C++那樣使用通用的宏就可以實現。
如果你有更好的方案,歡迎留言告訴我!