原文地址: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依然不容許這樣使用這個指針。ui
既然Golang不容許使用nil指針,那麼咱們能夠經過建立一個無用的T對象來繞開這個問題,代碼以下:spa
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。code
考慮到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指針便可?嘗試寫以下代碼並進行測試:get
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,代碼以下:string
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++那樣使用通用的宏就能夠實現。
若是你有更好的方案,歡迎留言告訴我!