1. 程式人生 > >在Golang裡如何實現結構體成員指標到結構體自身指標的轉換

在Golang裡如何實現結構體成員指標到結構體自身指標的轉換

在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++那樣使用通用的巨集就可以實現。

如果你有更好的方案,歡迎留言告訴我!

相關推薦

結構成員結構指標初始化

#include<stdio.h>#include <stdlib.h>#include <string>struct student{   char *name;   int score;   struct student* next;

C 語言通過結構成員獲得結構指標

通過結構體成員拿到結構體的指標,是C語言實現繼承多型的基礎。面向物件C程式設計可以參看這裡OOC 面向物件C語言程式設計實踐。這裡詳細介紹這個核心的操作方法。 /** * Get struct pointer from member pointer */ #defin

Golang如何實現結構成員指標結構自身指標轉換

在C語言中有一個經典的巨集定義,可以將結構體struct內部的某個成員的指標轉化為結構體自身的指標。下面是一個例子,通過FIELD_OFFSET巨集計算結構體內一個欄位的偏移,函式getT可以從一個F*的指標獲得對應的T*物件。 struct F { int c; int d; }

C++類 給結構成員 函式指標 賦值

myStruct標頭檔案 myStruct.h class CMyClass; struct {  int nFlag;  void (CMyClass::*myinit)(int n);  void (CMyClass::*myopen)(int n,void* arg)

list用remove實現結構成員的刪除

1、使用list,首先要包含list.h標頭檔案,並使用std名稱空間     在標頭檔案中增加如下兩行說明: #include <list>using namespace std; 2、定義結構體,需要在結構體裡寫判斷“==”函式,如下所列,這裡重寫的

指向結構指標&結構成員指標

1、指向結構體的指標 一個 變數的指標,就是該變數所佔據的記憶體段的起始地址。指向一個結構體的指標變數,其值是結構體變數的起始地址。 /* *copyright(c) 2018,HH *All rights reserved. *作 者:HH *完成日期:2018年8月1

關於stl::vector中儲存帶指標型別成員結構指標

最近用到vector中儲存結構體: struct sProc { // 程序ID int pid; // 程序狀態 int stat; // 狀態為0次數 int count; char *path; sProc() { pid = -1; stat = 0; cou

結構指標變數與結構成員指標變數

C程式碼 #include <stdio.h> #include <stdlib.h> #include <string.h> struct student{    char *name;    int score;  

[Go] golang結構成員與函數類型

邏輯 true div ring int pac return 結構體 new package main import ( "fmt" ) //定義一個類型 type tsh struct { //定義成員,類型是func() string test func(

函數外面對單個結構成員進行賦值出錯

類型 自動 構造 不能 結構體成員 bsp 入口 出錯 進入 關於“為什麽整型的就可以,結構體類型的就不能這麽賦值呢?”——整形等常規數據類型由編譯器自動識別,而自定義的數據類型(樓主自定義的結構體類型),編譯器在編譯階段無法識別,故出錯。 關於“在函數外單個初始化”——在

3.c語音結構成員內存對齊詳解

定義 pre 形狀 sed 兩個 分配 我們 替代 images 一.關鍵一點 最關鍵的一點:結構體在內存中是一個矩形,而不是一個不規則形狀 二.編程實戰 1 #include <stdlib.h> 2 #inc

計算C結構成員偏移量兩種方式本質上是一樣的

BE main print tdd of函數 pan color c結構體 計算 #include <stdio.h> #include <stddef.h> typedef struct test_st { char a[3];

strcpy拷貝結構成員中的字元陣列溢位的問題

結構體定義: typedef struct env {     char env_name[10];     char env_val[20];     int is_used;    

C/C++結構成員偏移量獲取

分析程式碼節選自muduo.   以下程式碼通過offsetof獲取sin_family在sockaddr_in6中的欄位偏移量. static_assert(offsetof(sockaddr_in6, sin6_family) == 0, "sin6_family offset 0"

Golang 之 面向物件struct ,定義結構方法(二)

package main import "fmt" type treeNode struct { value int left, right * treeNode } // 給結構體定義列印方法,其中(node treeNode)表示該方法的接受者是那個結構體 fun

求助!結構的二級指標陣列給一級指標初始化遇見的異常

百度也看了很多部落格都沒解決 主要程式碼如下 typedef struct HTNode { int weight; char c;//存這個字元,單個字元,符號都是葉子節點 int code; HTNode *lchild, rchild; }HuffmanTree; /

結構成員的記憶體分佈與對齊

我們先看一道IBM和微軟的筆試題: IBM筆試題: struct{  short   a1; short   a2;  short   a3;  }A;  struct{  long &n

static 指標 結構使用

static 指標 結構體使用 static使用注意事項 static --> //修飾區域性變數:將變數的生命週期變為和整個程式相同 但是不改變作用域 //修飾全域性變數:將改變作用域為當前檔案 //修飾一個函式:修改了函式的作用域為當前檔案 指標定義

@結構陣列指向結構變數的指標

一、結構體陣列的定義 struct student { int num; char name[20]; char sex; int age; float score; char addr[30]; } ; [stru

C語言:存取結構成員的點運算子(.)和箭頭運算子(->)的區別

一直以為這兩個是沒有什麼區別的,可以相互替換,今天又翻了一下《C語言核心技術》,明白了其中的奧妙。 相同點:兩個都是二元操作符,其右操作符是成員的名稱。 不同點:點操作符左邊的運算元是一個“結果為結構”的表示式; 箭頭操作符左邊的運算元是