網上有很多關于pos機顯示調試版本,1.14版本defer性能大幅度提升的知識,也有很多人為大家解答關于pos機顯示調試版本的問題,今天pos機之家(www.www690aa.com)為大家整理了關于這方面的知識,讓我們一起來看下吧!
本文目錄一覽:
pos機顯示調試版本
你的留言,是我堅持分享的動力。如果你有任何疑問,可以點擊關注,點贊、留言提問。
GO中的defer會在當前函數返回前執行傳入的函數,常用于關閉文件描述符,關閉鏈接及解鎖等操作。
Go語言中使用defer時會遇到兩個常見問題:
defer的調用時機和執行順序defer調用函數使用傳值的方法會執行函數,導致不符預期的結果接下來我們來詳細處理這兩個問題。
一、defer官方有段對defer的解釋:
每次defer語句執行時,會把函數“壓棧”,函數的參數會被拷貝下來,當外層函數退出時,defer函數按照定義的逆序執行,如果defer執行的函數為nil,那么會在最終調用函數產生panic。
這里我們先來一道經典的面試題
func main() {a,b := 1,2defer cal("1",a,cal("10",a,b))a = 0defer cal("2",a,cal("20",a,b))}func cal(index string, a, b int) int {ret := a + bfmt.Println(index,a,b,ret)return ret}
你覺得這個會打印什么?
輸出結果:
10 1 2 320 0 2 22 0 2 21 1 3 4
這里是遵循先入后出的原則,同時保留當前變量的值。
把這道題簡化一下:
func main() {var i = 1defer fmt.Println(i)i = 4}
輸出結果
1
上述代碼輸出似乎不符合預期,這個現象出現的原因是什么呢?經過分析,我們發現調用defer關鍵字會立即拷貝函數中引用的外部參數,所以fmt.Println(i)的這個i是在調用defer的時候就已經賦值了,所以會直接打印1。
想要解決這個問題也很簡單,只需要向defer關鍵字傳入匿名函數
func main() {var i = 1defer func() {fmt.Println(i)}()i = 4}二、數據結構
// 占用48字節的內存,link把所有的_defer串成一個鏈表。// _defer 只是一個header,結構緊跟的是延遲函數的參數和返回值的空間,大小由 siz 決定type _defer struct {// 參數和返回值的內存大小siz int32 started bool// 區分該結構是在棧上分配還是在堆上分配heap boolopenDefer bool// sp 計數器值,棧指針sp uintptr // sp at time of defer// pc 計數器值,程序計數器pc uintptr // pc at time of defer// defer 傳入的函數地址,也就是延后執行的函數fn *funcval // can be nil for open-coded defers // 觸發延遲調用的結構體_panic *_panic // 下一個要被執行的延遲函數link *_defer }
這里把一些垃圾回收使用的字段忽略了。
2.1、執行機制中間代碼生成階段cmd/compile/internal/gc/ssa.go會處理程序中的defer,該函數會根據條件不同,使用三種機制來處理該關鍵字
func (s *state) stmt(n *Node) {...switch n.Op {case ODEFER:if Debug_defer > 0 {var defertype stringif s.hasOpenDefers {defertype = "open-coded"} else if n.Esc == EscNever {defertype = "stack-allocated"} else {defertype = "heap-allocated"}Warnl(n.Pos, "%s defer", defertype)}if s.hasOpenDefers { // 開放編碼s.openDeferRecord(n.Left) } else { // 堆分配d := callDeferif n.Esc == EscNever { // 棧分配d = callDeferStack}s.call(n.Left, d)}}}
開放編碼、堆分配和棧分配是defer關鍵字的三種方法,而Go1.14加入的開放編碼,使得關鍵字開銷可以忽略不計。
2.2、堆分配call方法會為所有函數和方法調用生成中間代碼,工作內容:
獲取需要執行的函數名、閉包指針、代碼指針和函數調用的接收方獲取棧地址并將函數或方法寫入棧中調用newValue1A及相關函數生成函數調用的中間代碼如果當前函數的調用函數是defer,那么會單獨生成相關的結束代碼塊獲取函數的返回值并結束當前調用func (s *state) call(n *Node, k callKind) *ssa.Value {var call *ssa.Valueif k == callDeferStack { // 在棧上初始化defer結構體 ... } else { ... switch { case k == callDefer:call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem()) ... } call.AuxInt = stksize } s.vars[&memVar] = call}
defer關鍵字在運行時會調用deferproc,這個函數實現在src/runtime/panic.go里,接受兩個參數:參數的大小和閉包所在的地址。
編譯器不僅將defer關鍵字轉成deferproc函數,還會通過以下三種方式為所有調用defer的函數末尾插入deferreturn的函數調用
1、在cmd/compile/internal/gc/walk.go的walkstmt函數中,在遇到ODEFFER節點時會執行Curfn.Func.SetHasDefer(true),設置當前函數的hasdefer屬性
2、在ssa.go的buildssa會執行s.hasdefer = fn.Func.HasDefer()更新hasdefer
3、在exit中會根據hasdefer在函數返回前插入deferreturn的函數調用
func (s *state) exit() *ssa.Block {if s.hasdefer {if s.hasOpenDefers {...} else {s.rtcall(Deferreturn, true, nil)}} ...}2.2.1、創建延遲調用
runtime.deferproc為defer創建了一個runtime._defer結構體、設置它的函數指針fn、程序計數器pc和棧指針sp并將相關參數拷貝到相鄰的內存空間中
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fngp := getg()if gp.m.curg != gp {// go code on the system stack can't deferthrow("defer on system stack")}// 獲取caller的sp寄存器和pc寄存器值sp := getcallersp()argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)callerpc := getcallerpc()// 分配 _defer 結構d := newdefer(siz)if d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}// _defer 結構初始化d.link = gp._defergp._defer = dd.fn = fnd.pc = callerpcd.sp = spswitch siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))default:memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))}return0()}
最后調用的return0是唯一一個不會觸發延遲調用的函數,可以避免deferreturn的遞歸調用。
newdefer的分配方式是從pool緩存池中獲取:
從調度器的deferpool中取出結構體并將該結構體加入到當前goroutine的緩存池中從goroutine的deferpool中取出結構體通過mallocgc從堆上創建一個新的結構體func newdefer(siz int32) *_defer {var d *_defersc := deferclass(uintptr(siz))gp := getg() // 獲取當前的goroutine// 從pool緩存池中分配,如果沒有則mallocgc從堆上分配內存if sc < uintptr(len(p{}.deferpool)) {pp := gp.m.p.ptr()if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {d := sched.deferpool[sc]sched.deferpool[sc] = d.linkd.link = nilpp.deferpool[sc] = append(pp.deferpool[sc], d)}}if n := len(pp.deferpool[sc]); n > 0 {d = pp.deferpool[sc][n-1]pp.deferpool[sc][n-1] = nilpp.deferpool[sc] = pp.deferpool[sc][:n-1]}}if d == nil {total := roundupsize(totaldefersize(uintptr(siz)))d = (*_defer)(mallocgc(total, deferType, true))}d.siz = sizd.heap = truereturn d}
這三種方式取到的結構體_defer,都會被添加到鏈表的隊頭,這也是為什么defer按照后進先出的順序執行。
2.2.2、執行延遲調用func deferreturn(arg0 uintptr) { // 獲取當前的goroutinegp := getg() // 拷貝延遲函數到變量d上d := gp._defer // 如果延遲函數不存在則返回if d == nil {return}// 獲取caller函數的sp寄存器sp := getcallersp()if d.sp != sp { // 說明這個_defer 不是在該caller函數注冊的return}switch d.siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))default:memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))} // 獲取要調用的函數fn := d.fn // 重置函數d.fn = nil // 把下一個_defer結構體依附到goroutine上gp._defer = d.link // 釋放_defer(主要是堆回收,因為棧上的隨函數執行完會自動回收)freedefer(d) _ = fn.fn // 調用該函數jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
deferreturn就是從鏈表的隊頭取出并調用jmpdefer傳入需要執行的函數和參數。
該函數只有在所有延遲函數都執行后才會返回。
2.3、棧分配如果我們能夠將部分結構體分配到棧上就可以節約內存分配帶來的額外開銷。
在call函數中有在棧上分配
func (s *state) call(n *Node, k callKind) *ssa.Value {var call *ssa.Valueif k == callDeferStack { t := deferstruct(stksize) ... call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem())if stksize < int64(width="360px",height="auto" />
在運行期間deferprocStack只需要設置一些未在編譯期間初始化的字段,就可以將棧上的_defer追加到函數的鏈表上。
func deferprocStack(d *_defer) {gp := getg()if gp.m.curg != gp {// go code on the system stack can't deferthrow("defer on system stack")}// siz 和 fn在進入到這個函數之前已經賦值d.started = false// 標明是棧的內存d.heap = falsed.openDefer = false// 獲取到caller函數的sp寄存器值和pc寄存器d.sp = getcallersp()d.pc = getcallerpc()d.framepc = 0d.varp = 0*(*uintptr)(unsafe.Pointer(&d._panic)) = 0*(*uintptr)(unsafe.Pointer(&d.fd)) = 0*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))return0()}
除了分配的位置和堆的不同,其他的大致相同。
2.4、開放編碼Go語言在1.14中通過開放編碼實現defer關鍵字,使用代碼內聯優化defer關鍵的額外開銷并引入函數數據funcdata管理panic的調用,該優化可以將 defer 的調用開銷從 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而開放編碼作為一種優化 defer 關鍵字的方法,它不是在所有的場景下都會開啟的,開放編碼只會在滿足以下的條件時啟用:
函數的 defer 數量少于或者等于 8 個;函數的 defer 關鍵字不能在循環中執行;函數的 return 語句與 defer 語句的乘積小于或者等于 15 個;2.4.1、啟動優化const maxOpenDefers = 8func walkstmt(n *Node) *Node { ... switch n.Op { case ODEFER:Curfn.Func.SetHasDefer(true)Curfn.Func.numDefers++if Curfn.Func.numDefers > maxOpenDefers {Curfn.Func.SetOpenCodedDeferDisallowed(true)}if n.Esc != EscNever {Curfn.Func.SetOpenCodedDeferDisallowed(true)}fallthrough }}
如果函數中defer關鍵字的數量多于8個或者defer處于循環中,那么就會禁用開放編碼優化。
func buildssa(fn *Node, worker int) *ssa.Func { ...s.hasOpenDefers = Debug['N'] == 0 && s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed() if s.hasOpenDefers &&s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {s.hasOpenDefers = false} ...}
可以看到這里,判斷編譯參數不用-N,返回語句的數量和defer數量的乘積小于15,會啟用開放編碼優化。
2.4.2、延遲記錄延遲比特deferBitsTemp和延遲記錄是使用開放編碼實現defer的兩個最重要的結構,一旦使用開放編碼,buildssa會在棧上初始化大小為8個比特的deferBits
func buildssa(fn *Node, worker int) *ssa.Func {if s.hasOpenDefers {deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8])s.deferBitsTemp = deferBitsTempstartDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])s.vars[&deferBitsVar] = startDeferBitss.deferBitsAddr = s.addr(deferBitsTemp, false)s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)}}
延遲比特中的每一個比特位都表示該位對應的defer關鍵字是否需要被執行。延遲比特的作用就是標記哪些defer關鍵字在函數中被執行,這樣就能在函數返回時根據對應的deferBits確定要執行的函數。
而deferBits的大小為8比特,所以該優化的條件就是defer的數量小于8.
而執行延遲調用的時候仍在deferreturn
func deferreturn(arg0 uintptr) { if d.openDefer {done := runOpenDeferFrame(gp, d)if !done {throw("unfinished open-coded defers in deferreturn")}gp._defer = d.linkfreedefer(d)return}}
這里做了特殊的優化,在runOpenDeferFrame執行開放編碼延遲函數
1、從結構體_defer讀取deferBits,執行函數等信息
2、在循環中依次讀取執行函數的地址和參數信息,并通過deferBits判斷是否要執行
3、調用reflectcallSave執行函數
func runOpenDeferFrame(gp *g, d *_defer) bool {done := truefd := d.fddeferBitsOffset, fd := readvarintUnsafe(fd)nDefers, fd := readvarintUnsafe(fd)deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset)))for i := int(nDefers) - 1; i >= 0; i-- {// 讀取函數funcdata地址和參數信息var argwidth="360px",height="auto" />
三、總結1、新加入的defer放入隊頭,執行defer時是從隊頭取函數調用,所以是后進先出
2、通過判斷defer關鍵字、return數量來判斷是否開啟開放編碼優化
3、調用deferproc函數創建新的延遲調用函數時,會立即拷貝函數的參數,函數的參數不會等到真正執行時計算
以上就是關于pos機顯示調試版本,1.14版本defer性能大幅度提升的知識,后面我們會繼續為大家整理關于pos機顯示調試版本的知識,希望能夠幫助到大家!
