原文地址: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 int b int f F } type F struct { c int d int } func (m *F) T1() *T { var dummy *T fieldOffset := 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 T fieldOffset := 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 T for i := 0; i < b.N; i++ { if &t != t.f.T2() { b.Fatal("wrong") } } } func BenchmarkGetPtrByMemberPtr_TBad(b *testing.B) { var t T for 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 int b int f F e [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++那樣使用通用的宏就可以實現。
如果你有更好的方案,歡迎留言告訴我!
