分享
  1. 首页
  2. 文章

Golang升级到1.7后,之前正确的函数出现错误,分析原因及解决办法

GoWKH · · 2973 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

最近尝试把开发环境,升级到Golang1.7.1后,程序会偶发性的宕掉,查看日志后,发现总是在一个计算切片的哈希值的地方,错误信息是:

unexpected fault address 0xc043df4000,
fatal error: fault

在1.7之前程序持续运行2年了,从来没有出现这个问题,怀疑是Golang编译器升级到SSA后导致的。将程序的代码精简为以下函数:

//本代码的主要作用是,把一个字符串的Assii的值累加起来。
funcSimpleCrc(ptr uintptr, size int)int {
 ret := 0
 maxPtr := ptr + uintptr(size)
 for ptr < maxPtr {
 b := *(*byte)(unsafe.Pointer(ptr)) //出错的地方
 ret += int(b)
 ptr++
 }
 return ret
}

注:实际的代码比这个复杂很多。采用类似这种写法后,相比常规写法性能提升高达8倍。

分析错误直接表现是"非法内存地址访问"导致的,只有一种原因是"字符串使用的内存被SSA编译释放了",被GC提前回收了并且归还给了windows操作系统。因此查阅了SSA编译器的原理。发现SSA编译器变得聪明很多,它能根据(既定规则)快速判断出,内存不再被使用,所以内存回收非常迅速。由此思考的着眼点变为:有没有什么办法告知SSA编译器,特定的内存在指定的代码区不要回收?,记得之前看过Golang1.7在runtime包中,增加一个函数func KeepAlive(interface{}) {},查看注释后发现"使用该函数可以设定内存在指定的代码区保持有效",而不被GC回收。

为了重现上述推断,因此编写以下示例:

// memTest
package main
import (
 "fmt"
 "reflect"
 "runtime"
 "unsafe"
)
funcSimpleCrc(ptr uintptr, size int)int {
 ret := 0
 maxPtr := ptr + uintptr(size)
 for ptr < maxPtr {
 b := *(*byte)(unsafe.Pointer(ptr))
 ret += int(b)
 ptr++
 }
 return ret
}
//模拟申请内存,触发Gc回收内存
funcAllocation(size int) {
 var free []byte
 free = make([]byte, size)
 if len(free) == 0 {
 panic("Allocation Error")
 }
}
funcSliceCrcTest(slice []byte, N int)(ret int) {
 newSlice := []byte(string(slice)) //获取独立内存
 sh := (*reflect.SliceHeader)(unsafe.Pointer(&newSlice)) //反射切片结构
 ptr, size := uintptr(sh.Data), sh.Len //获取地址尺寸
 runtime.GC() //强制内存回收
 for i := 0; i < N; i++ {
 ret = SimpleCrc(ptr, size) //计算crc校验码
 Allocation(size) //模拟申请内存,触发Gc回收内存
 }
 //runtime.KeepAlive(newSlice) //本行一旦注释后结果不再是1665,取消注释节正确
 return
}
funcStringCrcTest(str string, N int)(ret int) {
 newStr := string([]byte(str)) //获取独立内存
 runtime.SetFinalizer(&newStr, func(x *string) {}) //设置回收事件
 sh := (*reflect.StringHeader)(unsafe.Pointer(&newStr)) //反射字符串结构
 ptr, size := uintptr(sh.Data), sh.Len //获取地址尺寸
 runtime.GC() //强制内存回收
 for i := 0; i < N; i++ {
 ret = SimpleCrc(ptr, size) //计算crc校验码
 Allocation(size) //模拟申请内存,触发Gc回收内存
 }
 //runtime.KeepAlive(newStr) //本行一旦注释后结果不再是1665,取消注释节正确
 return
}
funcmain() {
 var B = []byte("1234567890-1234567890-1234567890") //Crc的值为:1665
 var S = string(B) //生成字符串
 N := 1000000 //循环执行1,000,000次
 fmt.Printf("SimpleCrc(\"%s\") = %v\n", B, SliceCrcTest(B, N))
 fmt.Printf("SimpleCrc(\"%s\") = %v\n", B, StringCrcTest(S, N))
}

上述代码重现的思路是,首先申请内存,具体是new一个切片或字符串(其值是"1234567890-1234567890-1234567890",它的正确CRC结果是1665),分别传入函数SliceCrcTest和StringCrcTest查看运行结果;这里只介绍SliceCrcTest函数的内部实现思路,StringCrcTest和SliceCrcTest非常一致,请自己分析理解。

在SliceCrcTest函数内部,首先是代码

newSlice := []byte(string(slice)) //获取独立内存

本行代码重复申请了两次内存,其目的是,产生一个局部变量,加快重现GC回收newSlice。

sh := (*reflect.SliceHeader)(unsafe.Pointer(&newSlice)) //反射切片结构

本行代码是通过反射,获取到切片newSlice的数据结构,目的是读取"1234567890-1234567890-1234567890"的首地址和长度。

ptr, size := uintptr(sh.Data), sh.Len //获取地址尺寸

本行代码是获取"1234567890-1234567890-1234567890"的首地址和长度,到变量ptr, size。

runtime.GC() //强制内存回收

本行代码是强制启动内存回收扫描,然后for循环一百万次,这样做的目的是留出足够的时间让GC取回收内存,循环体类执行代码如下。

ret = SimpleCrc(ptr, size) //计算crc校验码
Allocation(size) //模拟申请内存,触发Gc回收内存

调用SimpleCrc计算"1234567890-1234567890-1234567890"的校验码,并把最后一次的结果保存到ret返回变量(正确值是1665)。Allocation函数是模拟申请一次内存,函数返回后就内存会被GC回收。

//runtime.KeepAlive(newSlice) //本行一旦注释后结果不再是1665,取消注释节正确

这条语句最为关键,本语句被注释了,那么SliceCrcTest的结果应该是0,这代表着,newSlice 内存被GC回收了,并且同一块内存被再次分配给Allocation函数中的free变量,由于free的初始化为由32个‘0’组成的切片,因此SliceCrcTest计算结果变成了"0"。这样就问题重现了,被SSA编译器误认为,内存不在有效,因此GC就会回收。

注:在实际的重现过程中,因为这是一个随机的过程,不同的操作系统可能不会重现,但是只要知道思路和原理,稍微调整一下N的数值,把它加大就会重现。

 N := 1000000 //循环执行1,000,000次

总结: 由于Golang的SSA的编译器,变得非常聪明了,因此会把使用反射reflect.StringHeader,reflect.SliceHeader返回值中的uintptr指向的内存块,当成了没有被使用的内存块回收了。

解决办法有两个:

一是尽量不要过分追求性能,使用反射reflect和unsafe包内的函数。这样能避免一些诡异的、很难分析的bug出现。 如果非要使用反射reflect和unsafe包内的函数,请注意一定要使用runtime.KeepAlive告诉SSA编译器,在指定的代码段内,不要回收内存块。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2973 次点击
1 回复 | 直到 1999年12月31日 16:00:00
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏