文档章节

在Golang里如何实现结构体成员指针到结构体自身指针的转换

seis
 seis
发布于 2017/07/29 01:39
字数 997
阅读 129
收藏 0

原文地址: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++那样使用通用的宏就可以实现。

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

© 著作权归作者所有

seis
粉丝 1
博文 4
码字总数 2581
作品 1
程序员
私信 提问
『Go 语言学习专栏』-- 第四期

大家好,我是谢伟,是一名程序员。 本专栏的主旨是:梳理 Golang 知识,力求从初级水平提升至中级水平。 同时将一些符合人性的编程经验和好的编程方法,分享给大家。 希望对大家有帮助。 本节...

谢小路
2018/05/10
0
0
golang: 利用unsafe操作未导出变量

看了 @喻恒春 大神的利用unsafe.Pointer来突破私有成员,觉得例子举得不太好。而且不应该简单的放个demo,至少要讲一下其中的原理,让看的童鞋明白所以然。see:http://my.oschina.net/achun...

陈亦
2014/01/17
5.5K
10
《Objective-C高级编程》温故知新之"Blocks"

前言 很久前看了《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书,但当时看起来晦涩难懂。最近利用下班时间重读了一遍,觉得还是得记录一下。毕竟每个阶段对相同的东西会有更深刻的...

Dwyane_Coding
2018/08/28
0
0
golang 内存分析之字节对齐规则

c 的字节对齐很重要,因为c 支持指针运算。在golang 里面一般是慎用指针运算的,所以,这部分很少用,但是有些场景为了性能不得不用到指针运算,这个时候,知道golang 的内存分配就很重要了。...

鼎铭
2018/06/12
601
0
Go 语言之 struct 结构体

struct 结构 Go中的struct与C语言中的struct非常相似,并且Go没有class 使用type struct{} 定义结构,名称遵循可见性规则 支持指向自身的指针类型成员 支持匿名结构,可用作成员或定义成员变...

故新
2018/07/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

3_数组

3_数组

行者终成事
13分钟前
2
0
经典系统设计面试题解析:如何设计TinyURL(二)

原文链接:https://www.educative.io/courses/grokking-the-system-design-interview/m2ygV4E81AR 编者注:本文以一道经典的系统设计面试题:《如何设计TinyURL》的参考答案和解析为例,帮助...

APEMESH
今天
7
0
使用logstash同步MySQL数据到ES

概述   在生成业务常有将MySQL数据同步到ES的需求,如果需要很高的定制化,往往需要开发同步程序用于处理数据。但没有特殊业务需求,官方提供的logstash就很有优势了。   在使用logstas...

zxiaofan666
今天
10
0
X-MSG-IM-分布式信令跟踪能力

经过一周多的鏖战, X-MSG-IM的分布式信令跟踪能力已基本具备, 特点是: 实时. 只有要RX/TX就会实时产生信令跟踪事件, 先入kafka, 再入influxdb待查. 同时提供实时sub/pub接口. 完备. 可以完整...

dev5
今天
7
0
OpenJDK之CyclicBarrier

OpenJDK8,本人看的是openJDK。以前就看过,只是经常忘记,所以记录下 图1 CyclicBarrier是Doug Lea在JDK1.5中引入的,作用就不详细描述了,主要有如下俩个方法使用: await()方法,如果当前线...

克虏伯
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部