Golang升級到1.7後,之前正確的函數出現錯誤,分析原因及解決辦法 轉

最近嘗試把開發環境,升級到Golang1.7.1後,程序會偶發性的宕掉,查看日誌後,發現總是在一個計算切片的哈希值的地方,錯誤信息是:

unexpected fault address 0xc043df4000,
fatal error: fault

在1.7之前程序持續運行2年了,從來沒有出現這個問題,懷疑是Golang編譯器升級到SSA後導致的。將程序的代碼精簡爲以下函數:

//本代碼的主要作用是,把一個字符串的Assii的值累加起來。
func SimpleCrc(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"
)

func SimpleCrc(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回收內存
func Allocation(size int) {
    var free []byte
    free = make([]byte, size)
    if len(free) == 0 {
        panic("Allocation Error")
    }
}

func SliceCrcTest(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
}

func StringCrcTest(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
}

func main() {
    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編譯器,在指定的代碼段內,不要回收內存塊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章