因为 interface 类型本质上就是 2 个 uintptr(一个表示 type 一个表示 value)。当你连 2 个 uintptr 都不想拷贝的时候,你就会用到指向 interface 的指针了。
天峨ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18982081108(备注:SSL证书合作)期待与您的合作!
当然,你用一个新的 uintptr 指向另一个 2 个 uintptr 长度的对象,也没省多少事儿
不知道你有没有听过这么一句:在使用 map 时尽量不要在 big map 中保存指针。好吧,你现在已经听过了:)为什么呢?原因在于 Go 语言的垃圾回收器会扫描标记 map 中的所有元素,GC 开销相当大,直接GG。
这两天在《Mastering Go》中看到 GC 这一章节里面对比 map 和 slice 在垃圾回收中的效率对比,书中只给出结论没有说明理由,这我是不能忍的,于是有了这篇学习笔记。扯那么多,Show Your Code
这是一个简单的测试程序,保存字符串的 map 和 保存整形的 map GC 的效率相差几十倍,是不是有同学会说明明保存的是 string 哪有指针?这个要说到 Go 语言中 string 的底层实现了,源码在 src/runtime/string.go里,可以看到 string 其实包含一个指向数据的指针和一个长度字段。注意这里的是否包含指针,包括底层的实现。
Go 语言的 GC 会递归遍历并标记所有可触达的对象,标记完成之后将所有没有引用的对象进行清理。扫描到指针就会往下接着寻找,一直到结束。
Go 语言中 map 是基于 数组和链表 的数据结构实现的,通过 优化的拉链法 解决哈希冲突,每个 bucket 可以保存 8 对键值,在 8 个键值对数据后面有一个 overflow 指针,因为桶中最多只能装 8 个键值对,如果有多余的键值对落到了当前桶,那么就需要再构建一个桶(称为溢出桶),通过 overflow 指针链接起来。
因为 overflow 指针的缘故,所以无论 map 保存的是什么,GC 的时候就会把所有的 bmap 扫描一遍,带来巨大的 GC 开销。官方 issues 就有关于这个问题的讨论, runtime: Large maps cause significant GC pauses #9477
无脑机翻如下:
如果我们有一个map [k] v,其中k和v都不包含指针,并且我们想提高扫描性能,则可以执行以下操作。
将“ allOverflow [] unsafe.Pointer”添加到 hmap 并将所有溢出存储桶存储在其中。 然后将 bmap 标记为noScan。 这将使扫描非常快,因为我们不会扫描任何用户数据。
实际上,它将有些复杂,因为我们需要从allOverflow中删除旧的溢出桶。 而且它还会增加 hmap 的大小,因此也可能需要重新整理数据。
最终官方在 hmap 中增加了 overflow 相关字段完成了上面的优化,这是具体的 commit 地址。
下面看下具体是如何实现的,源码基于 go1.15,src/cmd/compile/internal/gc/reflect.go 中
通过注释可以看出,如果 map 中保存的键值都不包含指针(通过 Haspointers 判断),就使用一个 uintptr 类型代替 bucket 的指针用于溢出桶 overflow 字段,uintptr 类型在 GO 语言中就是个大小可以保存得下指针的整数,不是指针,就相当于实现了 将 bmap 标记为 noScan, GC 的时候就不会遍历完整个 map 了。随着不断的学习,愈发感慨 GO 语言中很多模块设计得太精妙了。
差不多说清楚了,能力有限,有不对的地方欢迎留言讨论,源码位置还是问的群里大佬 _
这样。不过只是个精确到纳秒的计时器,不是精确到纳秒的当前时间。windows好像只能拿到ms精度的当前时间吧,不是很清楚。
package main
import (
"syscall"
"time"
"unsafe"
)
func NewStopWatch() func() time.Duration {
var QPCTimer func() func() time.Duration
QPCTimer = func() func() time.Duration {
lib, _ := syscall.LoadLibrary("kernel32.dll")
qpc, _ := syscall.GetProcAddress(lib, "QueryPerformanceCounter")
qpf, _ := syscall.GetProcAddress(lib, "QueryPerformanceFrequency")
if qpc == 0 || qpf == 0 {
return nil
}
var freq, start uint64
syscall.Syscall(qpf, 1, uintptr(unsafe.Pointer(freq)), 0, 0)
syscall.Syscall(qpc, 1, uintptr(unsafe.Pointer(start)), 0, 0)
if freq = 0 {
return nil
}
freqns := float64(freq) / 1e9
return func() time.Duration {
var now uint64
syscall.Syscall(qpc, 1, uintptr(unsafe.Pointer(now)), 0, 0)
return time.Duration(float64(now-start) / freqns)
}
}
var StopWatch func() time.Duration
if StopWatch = QPCTimer(); StopWatch == nil {
// Fallback implementation
start := time.Now()
StopWatch = func() time.Duration { return time.Since(start) }
}
return StopWatch
}
func main() {
// Call a new stop watch to create one from this moment on.
watch := NewStopWatch()
// Do some stuff that takes time.
time.Sleep(1)
// Call the stop watch itself and it will return a time.Duration
dur := watch()
}
这和语言没关系,操作系统要提供这样的原语。linux和windows都是可以的。
想进大厂,但不知道该如何入手,不妨从先过八股文的题量开始,比如先过个50题,然后一边面,一边学,进大厂就只不过是时间问题了,加油打工人!
本篇一共10题,大概花20分钟阅读。
1.golang的switch语句有什么特点?
switch关键字是通过对比key和case后面的value来选择需要执行的语句,与其他语言比如php和java不同的是,golang的switch默认不会去执行下一个case的语句,除非你显示的添加了一行fallthough关键字。
2.golang的select当有多个goroutine准备就绪,它是如何选择的?
select语句是用来处理与channel IO相关的逻辑,当有多个channel准备就绪的时候,其是伪随机选择一个goroutine来接收,然后执行相关的语句块。
Note: select关键字常用于和goroutine超时相关的逻辑设计。
3.golang什么时候会panic?
这里总结了8种,应付面试官应该是够了
4.子协程出现panic能在父协程使用recover()捕获吗?
不能,只能在子协程内部使用recover()捕获panic,协程只能捕获自己的panic。
5.什么样的panic不可恢复
6.defer函数的执行顺序是怎么样的?
7.unsafe.Pointer和uintptr是用来干什么的呢?
在golang中,为了安全性,是不允许指针像C++那样进行类型转换以及计算的,但是有些场景又必须要这么做怎么办呢?于是出现了unsafe.Pointer用于指针类型转换,比如*int64可以转换为*int64,出现了uintptr用于指针运算。
对于unsafe.Point有以下几点性质:
uintptr官方的定义是
其是用来做指针运算的
Note:
第三行的目的是为了获取age属性,age属性在stuct中处于第二列,首先是把student转换成unsafe.Point,获取指向student的指针,然后再转换成uintptr进行指针运算,然后通过unsafe.offset获取student.age相对于student的偏移量加上student的起始地址就能获得student.age的起始地址,然后转换成*int类型,就可以读取age属性了。
8.常用unsafe.Point和uintptr做什么呢,这么做有什么好处呢?
unsafe.Point常用于操作结构体的私有变量,以及类型转换。
好处就是golang中只有unsafe.Point能做到这个事,其他方法都做不到,反射的底层也是用unsafe.Point做的。
9.unsafe.Point和unintptr有什么坑呢?
千万要小心,不要为uintptr起一个中间变量 ,例如这样:
这是因为当发生gc的时候,可能会修改变量的内存地址,同时也会修改指向该变量的指针指向新的地址。但是uintptr是一个整数,其不是一个指针,因此在gc修改变量的时候,可不会修改它的值,他还指向原来的地址,然后转化成unsafe.Point进行操作,当然会报错。
10. string转byte的零拷贝技术
string在golang中的的存储结构为:
我们可以定义一个一样的结构体,然后用unsafe.Point把其转化成我们定义结构体,这样就可以把其私有属性,映射成共有属性了。
这个结构体golang已经帮我们定义好了,如下:
同理,slice的存储结构可以映射成,如下结构体,golang也已经帮我们定义好了
接来下就是具体的代码:
其实就是修改了一下byte切片data所指向的地址空间以及len就行了。
参考资料:
unsafe.Point
在Golang语言开发过程中,我们经常会用到数组和切片数据结构,数组是固定长度的,而切片是可以扩张的数组,那么切片底层到底有什么不同?接下来我们来详细分析一下内部实现。
首先我们来看一下数据结构
这里的array其实是指向切片管理的内存块首地址,而len就是切片的实际使用大小,cap就是切片的容量。
我们可以通过下面的代码输出slice:
这么分析下来,我们可以了解如下内容:
使用一个切片通常有两种方法:
另一种是slice = make([]int, len, cap)这种方法,称为分配内存。
创建一个slice,实质上是在分配内存。
这里跟一下细节,math.MulUintptr是基于底层的指针计算乘法的,这样计算不会导致超出int大小,这个方法在后面会经常用到。
同样,对于int64的长度,也有对应的方法
而实际分配内存的操作调用mallocgc这个分配内存的函数,这个函数以后再分析。
我们了解切片和数组最大的不同就是切片能够自动扩容,接下来看看切片是如何扩容的
这里可以看到,growslice是返回了一个新的slice,也就是说如果发生了扩容,会发生拷贝。
所以我们在使用过程中,如果预先知道容量,可以预先分配好容量再使用,能提高运行效率。
copy这个函数在内部实现为slicecopy
还有关于字符串的拷贝
这里显示了可以把string拷贝成[]byte,不能把[]byte拷贝成string。
1、切片的数据结构是 array内存地址,len长度,cap容量
2、make的时候需要注意 容量 * 长度 分配的内存大小要小于264,并且要小于可分配的内存量,同时长度不能大于容量。
3、内存增长的过程:
4、当发生内存扩容时,会发生拷贝数据的现象,影响程序运行的效率,如果可以,要先分配好指定的容量
5、关于拷贝,可以把string拷贝成[]byte,不能把[]byte拷贝成string。
售后响应及时
7×24小时客服热线数据备份
更安全、更高效、更稳定价格公道精准
项目经理精准报价不弄虚作假合作无风险
重合同讲信誉,无效全额退款