Go 語言之 defer 的前世今生

2021-03-06 CSDN

延遲語句 defer 在最早期的 Go 語言設計中並不存在,後來才單獨增加了這一特性,由 Robert Griesemer 完成語言規範的編寫 [Griesemer, 2009], 並由 Ken Thompson 完成最早期的實現 [Thompson, 2009],兩人合作完成這一語言特性。

defer 的語義表明,它會在函數返回、產生恐慌或者 runtime.Goexit 時被調用。直覺上看,defer 應該由編譯器直接將需要的函數調用插入到該調用的地方,似乎是一個編譯期特性,不應該存在運行時性能問題,非常類似於 C++ 的 RAII 範式(當離開資源的作用域時,自動執行析構函數)。但實際情況是,由於 defer 並沒有與其依賴資源掛鈎,也允許在條件、循環語句中出現,從而不再是一個作用域相關的概念,這就是使得 defer 的語義變得相對複雜。在一些複雜情況下,無法在編譯期決定存在多少個 defer 調用。

例如,在一個執行次數不確定的 for 循環中,defer 的執行次數是隨機的:

1func randomDefers() {
2  rand.Seed(time.Now().UnixNano())
3  for rand.Intn(100) > 42 {
4    defer func() {
5      println("changkun.de/golang")
6    }()
7  }
8}

因而 defer 並不是免費的午餐,在一個複雜的調用中,當無法直接確定需要的產生的延遲調用的數量時,延遲語句將導致運行性能的下降。本文我們來討論 defer 的實現本質及其對症下藥的相關性能優化手段。

defer 的類型延遲語句的文法產生式 DeferStmt -> "defer" Expression 的描述非常的簡單,因而也很容易將其處理為語法樹的形式,但我們這裡更關心的其實是它語義背後的中間和目標代碼的形式。在 《Go 語言原本》Go 程序編譯流程 一節中我們提到過,在進行中間代碼生成階段,會通過 compileSSA 先調用 buildssa 為函數體生成 SSA 形式的函數,而後調用 genssa 將函數的 SSA 中間表示轉換為具體的指令。Go 語言的語句在執行 buildssa 階段中,會由 state.stmt 完成函數中各個語句的 SSA 處理。

1// src/cmd/compile/internal/gc/ssa.go
2func buildssa(fn *Node, worker int) *ssa.Func {
3  var s state
4  ...
5  s.stmtList(fn.Nbody)
6  ...
7}
8func (s *state) stmtList(l Nodes) {
9  for _, n := range l.Slice() { s.stmt(n) }
10}

對於延遲語句而言,其中間表示會產生三種不同的延遲形式, 第一種是最一般情況下的在堆上分配的延遲語句,第二種是允許在棧上分配的延遲語句,最後一種則是**開放編碼式(Open-coded)**的延遲語句。

1// src/cmd/compile/internal/gc/ssa.go
2func (s *state) stmt(n *Node) {
3  ...
4  switch n.Op {
5  case ODEFER:
6    // 開放編碼式 defer
7    if s.hasOpenDefers {
8      s.openDeferRecord(n.Left)
9    } else {
10      // 堆上分配的 defer
11      d := callDefer
12      if n.Esc == EscNever {
13        // 棧上分配的 defer
14        d = callDeferStack
15      }
16      s.call(n.Left, d)
17    }
18  case ...
19  }
20  ...
21}


在堆上分配的 defer我們先來討論最簡單的在堆上分配的 defer 這種形式。在堆上分配的原因是 defer 語句出現在了循環語句裡,或者無法執行更高階的編譯器優化導致的。如果一個與 defer 出現在循環語句中,則可執行的次數可能無法在編譯期決定;如果一個調用中 defer 由於數量過多等原因,不能被編譯器進行開放編碼,則也會在堆上分配 defer。總之,由於這種不確定性的存在,在堆上分配的 defer 需要最多的運行時支持,因而產生的運行時開銷也最大。
編譯階段為了使延遲語句的功能滿足語言規範,該語句在編譯的 SSA 階段會被翻譯為兩個主體,其中第一個主體是被延遲的函數本身,另一個主體則是函數結束時需要執行所記錄 defer 的代碼塊。state.call 調用會生成用於記錄延遲調用參數的指令,並創建一個 deferproc 的調用指令;而後 state.exit 調用在函數返回前插入 deferreturn 調用的指令。

1// src/cmd/compile/internal/gc/ssa.go
2func (s *state) call(n *Node, k callKind) *ssa.Value {
3  ...
4  var call *ssa.Value
5  if k == callDeferStack {
6    ...
7  } else {
8    // 在堆上創建 defer
9    argStart := Ctxt.FixedFrameSize()
10    // Defer 參數
11    if k != callNormal {
12      // 記錄 deferproc 的參數
13      argsize := s.constInt32(types.Types[TUINT32], int32(stksize))
14      addr := s.constOffPtrSP(s.f.Config.Types.UInt32Ptr, argStart)
15      s.store(types.Types[TUINT32], addr, argsize)  // 保存參數大小 siz
16      addr = s.constOffPtrSP(s.f.Config.Types.UintptrPtr, argStart+int64(Widthptr))
17      s.store(types.Types[TUINTPTR], addr, closure)  // 保存函數地址 fn
18      stksize += 2 * int64(Widthptr)
19      argStart += 2 * int64(Widthptr)
20    }
21    ...
22
23    // 創建 deferproc 調用
24    switch {
25    case k == callDefer:
26      call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem())
27    ...
28    }
29    ...
30  }
31  ...
32
33  // 結束 defer 塊
34  if k == callDefer || k == callDeferStack {
35    s.exit()
36    ...
37  }
38  ...
39}
40func (s *state) exit() *ssa.Block {
41  if s.hasdefer {
42    if s.hasOpenDefers {
43      ...
44    } else {
45      // 調用 deferreturn
46      s.rtcall(Deferreturn, true, nil)
47    }
48  }
49  ...
50}

1package main
2
3func foo() {
4  return
5}
6
7func main() {
8  defer foo()
9  return
10}

如果我們將其強制編譯為在堆上分配的形式,可以觀察到如下的彙編代碼。其中 defer foo()被轉化為了 deferproc 調用,並在函數返回前,調用了 deferreturn:

1TEXT main.foo(SB) /Users/changkun/Desktop/defer/ssa/main.go
2  return
3  0x104ea20    c3      RET      
4
5TEXT main.main(SB) /Users/changkun/Desktop/defer/ssa/main.go
6func main() {
7  ...
8  // 將 defer foo() { ... }() 轉化為一個 deferproc 調用
9  // 在調用 deferproc 前完成參數的準備工作,這個例子中沒有參數
10  0x104ea4d    c7042400000000    MOVL $0x0, 0(SP)    
11  0x104ea54    488d0585290200    LEAQ go.func.*+60(SB), AX  
12  0x104ea5b    4889442408    MOVQ AX, 0x8(SP)    
13  0x104ea60    e8bb31fdff    CALL runtime.deferproc(SB)  
14  ...
15  // 函數返回指令 RET 前插入的 deferreturn 語句
16  0x104ea7b    90      NOPL        
17  0x104ea7c    e82f3afdff    CALL runtime.deferreturn(SB)  
18  0x104ea81    488b6c2410    MOVQ 0x10(SP), BP    
19  0x104ea86    4883c418    ADDQ $0x18, SP      
20  0x104ea8a    c3      RET        
21  // 函數的尾聲
22  0x104ea8b    e8d084ffff    CALL runtime.morestack_noctxt(SB)  
23  0x104ea90    eb9e      JMP main.main(SB)


運行階段一個函數中的延遲語句會被保存為一個 _defer 記錄的鍊表,附著在一個 Goroutine 上。_defer 記錄的具體結構也非常簡單,主要包含了參與調用的參數大小、當前 defer 語句所在函數的 PC 和 SP 寄存器、被 defer 的函數的入口地址以及串聯多個 defer 的 link 鍊表,該鍊表指向下一個需要執行的 defer,如圖 9.2.1 所示。

1// src/runtime/panic.go
2type _defer struct {
3  siz       int32
4  heap      bool
5  sp        uintptr
6  pc        uintptr
7  fn        *funcval
8  link      *_defer
9  ...
10}
11// src/runtime/runtime2.go
12type g struct {
13  ...
14  _defer *_defer
15  ...
16}

附著在 Goroutine 上的 _defer 記錄的鍊表現在我們知道,一個在堆上分配的延遲語句被編譯為了 deferproc,用於記錄被延遲的函數調用;在函數的尾聲,會插入 deferreturn 調用,用於執行被延遲的調用。下面我們就來詳細看看這兩個調用具體發生了什麼事情。我們先看創建 defer 的第一種形式 deferproc。這個調用很簡單,僅僅只是將需要被 defer 調用的函數做了一次記錄:

1//go:nosplit
2func deferproc(siz int32, fn *funcval) {
3  ...
4  sp := getcallersp()
5  argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
6  callerpc := getcallerpc()
7
8  d := newdefer(siz)
9  d.fn = fn
10  d.pc = callerpc
11  d.sp = sp
12
13  // 將參數保存到 _defer 記錄中
14  switch siz {
15  case 0: // 什麼也不做
16  case sys.PtrSize:
17    *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
18  default:
19    memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
20  }
21
22  return0()
23}

這段代碼中,本質上只是在做一些簡單參數處理,比如 fn 保存了 defer 所調用函數的調用地址,siz 確定了其參數的大小。並且通過 newdefer 來創建一個新的 _defer 實例,然後由 fn、callerpc 和 sp 來保存調用該 defer 的 Goroutine 上下文。注意,在這裡我們看到了一個對參數進行拷貝的操作。這個操作也是我們在實踐過程中經歷過的,defer 調用被記錄時,並不會對參數進行求值,而是會對參數完成一次拷貝。這麼做原因是由於語義上的考慮。直覺上講,defer 的參數應當在它所寫的位置對傳入的參數進行求值,而不是將求值步驟推遲,因為延後的參數可能發生變化,導致 defer 的語義發生意料之外的錯誤。例如,f, _ := os.Open("file.txt") 後立刻指定 defer f.Close(),倘若隨後的語句修改了 f 的值,那麼將導致 f 無法被正常關閉。出於性能考慮,newdefer 通過 P 或者調度器 sched 上的本地或全局 defer 池來復用已經在堆上分配的內存。defer 的資源池會根據被延遲的調用所需的參數來決定 defer 記錄的大小等級,每 16 個字節分一個等級。此做法的動機與運行時內存分配器針對不同大小對象的分配思路雷同,這裡不再做深入討論。

1// src/runtime/runtime2.go
2type p struct {
3  ...
4  // 不同大小的本地 defer 池
5  deferpool    [5][]*_defer
6  deferpoolbuf [5][32]*_defer
7  ...
8}
9type schedt struct {
10  ...
11  // 不同大小的全局 defer 池
12  deferlock mutex
13  deferpool [5]*_defer
14  ...
15}

‍對於新建的 _defer 實例而言,會將其加入到 Goroutine 所保留的 defer 鍊表上,通過 link 欄位串聯:

1// src/runtime/panic.go
2
3//go:nosplit
4func newdefer(siz int32) *_defer {
5  var d *_defer
6  sc := deferclass(uintptr(siz))
7  gp := getg()
8  // 檢查 defer 參數的大小是否從 p 的 deferpool 直接分配
9  if sc < uintptr(len(p{}.deferpool)) {
10    pp := gp.m.p.ptr()
11
12    // 如果 p 本地無法分配,則從全局池中獲取一半 defer,來填充 P 的本地資源池
13    if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
14      // 出於性能考慮,如果發生棧的增長,則會調用 morestack,
15      // 進一步降低 defer 的性能。因此切換到系統棧上執行,進而不會發生棧的增長。
16      systemstack(func() {
17        lock(&sched.deferlock)
18        for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
19          d := sched.deferpool[sc]
20          sched.deferpool[sc] = d.link
21          d.link = nil
22          pp.deferpool[sc] = append(pp.deferpool[sc], d)
23        }
24        unlock(&sched.deferlock)
25      })
26    }
27
28    // 從 P 本地進行分配
29    if n := len(pp.deferpool[sc]); n > 0 {
30      d = pp.deferpool[sc][n-1]
31      pp.deferpool[sc][n-1] = nil
32      pp.deferpool[sc] = pp.deferpool[sc][:n-1]
33    }
34  }
35  // 沒有可用的緩存,直接從堆上分配新的 defer 和 args
36  if d == nil {
37    systemstack(func() {
38      total := roundupsize(totaldefersize(uintptr(siz)))
39      d = (*_defer)(mallocgc(total, deferType, true))
40    })
41  }
42  // 將 _defer 實例添加到 Goroutine 的 _defer 鍊表上。
43  d.siz = siz
44  d.heap = true
45  d.link = gp._defer
46  gp._defer = d
47  return d
48}

deferreturn 被編譯器插入到函數末尾,當跳轉到它時,會將需要被 defer 的入口地址取出,然後跳轉並執行:

1// src/runtime/panic.go
2
3//go:nosplit
4func deferreturn(arg0 uintptr) {
5  gp := getg()
6  d := gp._defer
7  if d == nil {
8    return
9  }
10  // 確定 defer 的調用方是不是當前 deferreturn 的調用方
11  sp := getcallersp()
12  if d.sp != sp {
13    return
14  }
15  ...
16
17  // 將參數複製出 _defer 記錄外
18  switch d.siz {
19  case 0: // 什麼也不做
20  case sys.PtrSize:
21    *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
22  default:
23    memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
24  }
25  // 獲得被延遲的調用 fn 的入口地址,並隨後立即將 _defer 釋放掉
26  fn := d.fn
27  d.fn = nil
28  gp._defer = d.link
29  freedefer(d)
30
31  // 調用,並跳轉到下一個 defer
32  jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
33}

在這個函數中,會在需要時對 defer 的參數再次進行拷貝,多個 defer 函數以 jmpdefer 尾調用形式被實現。在跳轉到 fn 之前,_defer 實例被釋放歸還,jmpdefer 真正需要的僅僅只是函數的入口地址和參數,以及它的調用方 deferreturn 的 SP:

1// src/runtime/asm_amd64.s
2
3// func jmpdefer(fv *funcval, argp uintptr)
4TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
5  MOVQ  fv+0(FP), DX  // DX = fn
6  MOVQ  argp+8(FP), BX  // 調用方 SP
7  LEAQ  -8(BX), SP    // CALL 後的調用方 SP
8  MOVQ  -8(SP), BP    // 恢復 BP,好像 deferreturn 返回
9  SUBQ  $5, (SP)    // 再次返回到 CALL
10  MOVQ  0(DX), BX    // BX = DX
11  JMP  BX          // 最後才運行被 defer 的函數

這個 jmpdefer 巧妙的地方在於,它通過調用方 SP 來推算了 deferreturn的入口地址,從而在完成某個 defer 調用後,由於被 defer 的函數返回時會出棧,會再次回到 deferreturn 的初始位置,進而繼續反覆調用,從而模擬 deferreturn 不斷地對自己進行尾遞歸的假象。釋放操作非常普通,只是簡單地將其歸還到 P 的 deferpool 中, 並在本地池已滿時將其歸還到全局資源池:

1// src/runtime/panic.go
2
3//go:nosplit
4func freedefer(d *_defer) {
5  ...
6  sc := deferclass(uintptr(d.siz))
7  if sc >= uintptr(len(p{}.deferpool)) {
8    return
9  }
10  pp := getg().m.p.ptr()
11  // 如果 P 本地池已滿,則將一半資源放入全局池,同樣也是出於性能考慮
12  // 操作會切換到系統棧上執行。
13  if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
14    systemstack(func() {
15      var first, last *_defer
16      for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
17        n := len(pp.deferpool[sc])
18        d := pp.deferpool[sc][n-1]
19        pp.deferpool[sc][n-1] = nil
20        pp.deferpool[sc] = pp.deferpool[sc][:n-1]
21        if first == nil {
22          first = d
23        } else {
24          last.link = d
25        }
26        last = d
27      }
28      lock(&sched.deferlock)
29      last.link = sched.deferpool[sc]
30      sched.deferpool[sc] = first
31      unlock(&sched.deferlock)
32    })
33  }
34
35  // 恢復 _defer 的零值,即 *d = _defer{}
36  d.siz = 0
37  ...
38  d.sp = 0
39  d.pc = 0
40  d.framepc = 0
41  ...
42  d.link = nil
43
44  // 放入 P 本地資源池
45  pp.deferpool[sc] = append(pp.deferpool[sc], d)
46}

在棧上創建 deferdefer 還可以直接在棧上進行分配,也就是第二種記錄 defer 的形式 deferprocStack。在棧上分配 defer 的好處在於函數返回後 _defer 便已得到釋放,不再需要考慮內存分配時產生的性能開銷,只需要適當地維護 _defer 的鍊表即可。在 SSA 階段與在堆上分配的區別在於,在棧上創建 defer, 需要直接在函數調用幀上使用編譯器來初始化 _defer 記錄,並作為參數傳遞給 deferprocStack:

1// src/cmd/compile/internal/gc/ssa.go
2func (s *state) call(n *Node, k callKind) *ssa.Value {
3  ...
4  var call *ssa.Value
5  if k == callDeferStack {
6    // 直接在棧上創建 defer 記錄
7    t := deferstruct(stksize) // 從編譯器角度構造 _defer 結構
8    d := tempAt(n.Pos, s.curfn, t)
9
10    s.vars[&memVar] = s.newValue1A(ssa.OpVarDef, types.TypeMem, d, s.mem())
11    addr := s.addr(d, false)
12
13    // 在棧上預留記錄 _defer 的各個欄位的空間
14    s.store(types.Types[TUINT32],
15      s.newValue1I(ssa.OpOffPtr, types.Types[TUINT32].PtrTo(), t.FieldOff(0), addr),
16      s.constInt32(types.Types[TUINT32], int32(stksize)))
17    s.store(closure.Type,
18      s.newValue1I(ssa.OpOffPtr, closure.Type.PtrTo(), t.FieldOff(6), addr),
19      closure)
20
21    // 記錄參與 defer 調用的函數參數
22    ft := fn.Type
23    off := t.FieldOff(12)
24    args := n.Rlist.Slice()
25
26    // 調用 deferprocStack,以 _defer 記錄的指針作為參數傳遞
27    arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize())
28    s.store(types.Types[TUINTPTR], arg0, addr)
29    call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem())
30    ...
31  } else { ... }
32
33  // 函數尾聲與堆上分配的棧一樣,調用 deferreturn
34  if k == callDefer || k == callDeferStack {
35    ...
36    s.exit()
37  }
38  ...
39 }

可見,在編譯階段,一個 _defer 記錄的空間已經在棧上得到保留,deferprocStack 的作用就僅僅承擔了運行時對該記錄的初始化這一功能:

1// src/runtime/panic.go
2
3//go:nosplit
4func deferprocStack(d *_defer) {
5  gp := getg()
6  // 注意,siz 和 fn 已經在編譯階段完成設置,這裡只初始化了其他欄位
7  d.started = false
8  d.heap = false    // 可見此時 defer 被標記為不在堆上分配
9  d.openDefer = false
10  d.sp = getcallersp()
11  d.pc = getcallerpc()
12  ...
13  // 儘管在棧上進行分配,仍然需要將多個 _defer 記錄通過鍊表進行串聯,
14  // 以便在 deferreturn 中找到被延遲的函數的入口地址:
15  //   d.link = gp._defer
16  //   gp._defer = d
17  *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
18  *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
19  return0()
20 }

至於函數尾聲的行為,與在堆上進行分配的操作同樣是調用 deferreturn,我們就不再重複說明了。當然,裡面涉及的 freedefer 調用由於不需要釋放任何內存,也就早早返回了:

1// src/runtime/panic.go
2func freedefer(d *_defer) {
3  if !d.heap { return }
4  ...
5}


開放編碼式 defer正如本文最初所描述的那樣,defer 給我們的第一感覺其實是一個編譯期特性。前面我們討論了為什麼 defer 會需要運行時的支持,以及需要運行時的 defer 是如何工作的。現在我們來探究一下什麼情況下能夠讓 defer 進化為一個僅編譯期特性,即在函數末尾直接對延遲函數進行調用,做到幾乎不需要額外的開銷。這類幾乎不需要額外運行時性能開銷的 defer,正是開放編碼式 defer。這類 defer 與直接調用產生的性能差異有多大呢?我們不妨編寫兩個性能測試:

1func call()      { func() {}() }
2func callDefer() { defer func() {}() }
3func BenchmarkDefer(b *testing.B) {
4  for i := 0; i < b.N; i++ {
5    call() // 第二次運行時替換為 callDefer
6  }
7}

在 Go 1.14 版本下,讀者可以獲得類似下方的性能估計,其中使用 callDefer後,性能損耗大約為 1 ns。這種納秒級的性能損耗不到一個 CPU 時鐘周期,我們已經可以認為開放編碼式 defer 幾乎沒有了性能開銷:

1ame      old time/op  new time/op  delta
2Defer-12  1.24ns ± 1%  2.23ns ± 1%  +80.06%  (p=0.000 n=10+9)

我們再來觀察一下開放編碼式 defer 最終被編譯的形式:

1$ go build -gcflags "-l" -ldflags=-compressdwarf=false -o main.out main.go
2$ go tool objdump -S main.out > main.s

1var mu sync.Mutex
2func callDefer() {
3  mu.Lock()
4  defer mu.Unlock()
5}

整個調用最終編譯結果既沒有 deferproc 或者 deferprocStack,也沒有了 deferreturn。延遲語句被直接插入到了函數的末尾:

1TEXT main.callDefer(SB) /Users/changkun/Desktop/defer/main.go
2func callDefer() {
3  ...
4  mu.Lock()
5  0x105794a    488d05071f0a00    LEAQ main.mu(SB), AX    
6  0x1057951    48890424    MOVQ AX, 0(SP)      
7  0x1057955    e8f6f8ffff    CALL sync.(*Mutex).Lock(SB)  
8  defer mu.Unlock()
9  0x105795a    488d057f110200    LEAQ go.func.*+1064(SB), AX  
10  0x1057961    4889442418    MOVQ AX, 0x18(SP)    
11  0x1057966    488d05eb1e0a00    LEAQ main.mu(SB), AX    
12  0x105796d    4889442410    MOVQ AX, 0x10(SP)    
13}
14  0x1057972    c644240f00    MOVB $0x0, 0xf(SP)    
15  0x1057977    488b442410    MOVQ 0x10(SP), AX    
16  0x105797c    48890424    MOVQ AX, 0(SP)      
17  0x1057980    e8ebfbffff    CALL sync.(*Mutex).Unlock(SB)  
18  0x1057985    488b6c2420    MOVQ 0x20(SP), BP    
19  0x105798a    4883c428    ADDQ $0x28, SP      
20  0x105798e    c3      RET        
21  ...

那麼開放編碼式 defer 是怎麼實現的?所有的 defer 都是開放編碼式的嗎?什麼情況下,開放編碼式 defer 會退化為一個依賴運行時的特性?
產生條件我們先來看開放編碼式 defer 的產生條件。在 SSA 的構建階段 buildssa,我們有:

1// src/cmd/compile/internal/gc/ssa.go
2const maxOpenDefers = 8
3func walkstmt(n *Node) *Node {
4  ...
5  switch n.Op {
6  case ODEFER:
7    Curfn.Func.SetHasDefer(true)
8    Curfn.Func.numDefers++
9    // 超過 8 個 defer 時,禁用對 defer 進行開放編碼
10    if Curfn.Func.numDefers > maxOpenDefers {
11      Curfn.Func.SetOpenCodedDeferDisallowed(true)
12    }
13    // 存在循環語句中的 defer,禁用對 defer 進行開放編碼。
14    // 是否有 defer 發生在循環語句內,會在 SSA 之前的逃逸分析中進行判斷,
15    // 逃逸分析會檢查是否存在循環(loopDepth):
16    // if where.Op == ODEFER && e.loopDepth == 1 {
17    //   where.Esc = EscNever
18    //   ...
19    // }
20    if n.Esc != EscNever {
21      Curfn.Func.SetOpenCodedDeferDisallowed(true)
22    }
23  case ...
24  }
25  ...
26}
27
28func buildssa(fn *Node, worker int) *ssa.Func {
29  ...
30  var s state
31  ...
32  s.hasdefer = fn.Func.HasDefer()
33  ...
34  // 可以對 defer 進行開放編碼的條件
35  s.hasOpenDefers = Debug['N'] == 0 && s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
36  if s.hasOpenDefers &&
37    s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {
38    s.hasOpenDefers = false
39  }
40  ...
41}

這樣,我們得到了允許進行 defer 的開放編碼的主要條件(此處略去了一些常見生產環境無關的條件,例如啟用競爭檢查時也不能對 defer 進行開放編碼):沒有禁用編譯器優化,即沒有設置 -gcflags "-N"函數內 defer 的數量不超過 8 個、且返回語句與延遲語句個數的乘積不超過 15
延遲比特當然,正常編寫的 defer 可以直接被編譯器分析得出,但是如本文開頭提到的,如果一個 defer 發生在一個條件語句中,而這個條件必須等到運行時才能確定:

1if rand.Intn(100) < 42 {
2  defer fmt.Println("meaning-of-life")
3}

那麼如何才能使用最小的成本,讓插入到函數末尾的延遲語句,在條件成立時候被正確執行呢?這便需要一種機制,能夠記錄存在延遲語句的條件分支是否被執行,這種機制在 Go 中利用了延遲比特(defer bit)。這種做法非常巧妙,但原理卻非常簡單。

1defer f1(a1)
2if cond {
3  defer f2(a2)
4}
5...

使用延遲比特的核心思想可以用下面的偽代碼來概括。在創建延遲調用的階段,首先通過延遲比特的特定位置記錄哪些帶條件的 defer 被觸發。這個延遲比特是一個長度為 8 位的二進位碼(也是硬體架構裡最小、最通用的情況),以每一位是否被設置為 1,來判斷延遲語句是否在運行時被設置,如果設置,則發生調用。否則則不調用:

1deferBits = 0           // 初始值 00000000
2deferBits |= 1 << 0     // 遇到第一個 defer,設置為 00000001
3_f1 = f1
4_a1 = a1
5if cond {
6  // 如果第二個 defer 被設置,則設置為 00000011,否則依然為 00000001
7  deferBits |= 1 << 1
8  _f2 = f2
9  _a2 = a2
10}

在退出位置,再重新根據被標記的延遲比特,反向推導哪些位置的 defer 需要被觸發,從而執行延遲調用:

1exit:
2// 按順序倒序檢查延遲比特。如果第二個 defer 被設置,則
3//   00000011 & 00000010 == 00000010,即延遲比特不為零,應該調用 f2。
4// 如果第二個 defer 沒有被設置,則 
5//   00000001 & 00000010 == 00000000,即延遲比特為零,不應該調用 f2。
6if deferBits & 1 << 1 != 0 { // 00000011 & 00000010 != 0
7  deferBits &^= 1<<1       // 00000001
8  _f2(_a2)
9}
10// 同理,由於 00000001 & 00000001 == 00000001,因此延遲比特不為零,應該調用 f1
11if deferBits && 1 << 0 != 0 {
12  deferBits &^= 1<<0
13  _f1(_a1)
14}

在實際的實現中,可以看到,當可以設置開放編碼式 defer 時,buildssa 會首先創建一個長度位 8 位的臨時變量:

1// src/cmd/compile/internal/gc/ssa.go
2func buildssa(fn *Node, worker int) *ssa.Func {
3  ...
4  if s.hasOpenDefers {
5    // 創建 deferBits 臨時變量
6    deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8])
7    s.deferBitsTemp = deferBitsTemp
8    // deferBits 被設計為 8 位二進位,因此可以被開放編碼的 defer 數量不能超過 8 個
9    // 此處還將起始 deferBits 設置為零
10    startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])
11    s.vars[&deferBitsVar] = startDeferBits
12    s.deferBitsAddr = s.addr(deferBitsTemp, false)
13    s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)
14    ...
15  }
16  ...
17  s.stmtList(fn.Nbody) // 調用 s.stmt
18  ...
19}

‍‍‍

1// src/cmd/compile/internal/gc/ssa.go
2func (s *state) stmt(n *Node) {
3  ...
4  switch n.Op {
5  case ODEFER:
6    // 開放編碼式 defer
7    if s.hasOpenDefers {
8      s.openDeferRecord(n.Left)
9    } else { ... }
10  case ...
11  }
12  ...
13}
14
15// 存儲一個 defer 調用的相關信息,例如所在的語法樹結點、被延遲的調用、參數等等
16type openDeferInfo struct {
17  n           *Node
18  closure     *ssa.Value
19  closureNode *Node
20  ...
21  argVals     []*ssa.Value
22  argNodes    []*Node
23}
24func (s *state) openDeferRecord(n *Node) {
25  ...
26  var args []*ssa.Value
27  var argNodes []*Node
28
29  // 記錄與 defer 相關的入口地址與參數信息
30  opendefer := &openDeferInfo{n: n}
31  fn := n.Left
32  // 記錄函數入口地址
33  if n.Op == OCALLFUNC {
34    closureVal := s.expr(fn)
35    closure := s.openDeferSave(nil, fn.Type, closureVal)
36    opendefer.closureNode = closure.Aux.(*Node)
37    if !(fn.Op == ONAME && fn.Class() == PFUNC) {
38      opendefer.closure = closure
39    }
40  } else {
41    ...
42  }
43  // 記錄需要立即求值的的參數
44  for _, argn := range n.Rlist.Slice() {
45    var v *ssa.Value
46    if canSSAType(argn.Type) {
47      v = s.openDeferSave(nil, argn.Type, s.expr(argn))
48    } else {
49      v = s.openDeferSave(argn, argn.Type, nil)
50    }
51    args = append(args, v)
52    argNodes = append(argNodes, v.Aux.(*Node))
53  }
54  opendefer.argVals = args
55  opendefer.argNodes = argNodes
56
57  // 每多出現一個 defer,len(defers) 會增加,進而 
58  // 延遲比特 deferBits |= 1<<len(defers) 被設置在不同的位上
59  index := len(s.openDefers)
60  s.openDefers = append(s.openDefers, opendefer)
61  bitvalue := s.constInt8(types.Types[TUINT8], 1<<uint(index))
62  newDeferBits := s.newValue2(ssa.OpOr8, types.Types[TUINT8], s.variable(&deferBitsVar, types.Types[TUINT8]), bitvalue)
63  s.vars[&deferBitsVar] = newDeferBits
64  s.store(types.Types[TUINT8], s.deferBitsAddr, newDeferBits)
65 }

在函數返回退出前,state 的 exit 函數會依次倒序創建對延遲比特的檢查代碼,從而順序調用被延遲的函數調用:

1// src/cmd/compile/internal/gc/ssa.go
2func (s *state) exit() *ssa.Block {
3  if s.hasdefer {
4    if s.hasOpenDefers {
5      ...
6      s.openDeferExit()
7    } else {
8      ...
9    }
10  }
11  ...
12}
13
14func (s *state) openDeferExit() {
15  deferExit := s.f.NewBlock(ssa.BlockPlain)
16  s.endBlock().AddEdgeTo(deferExit)
17  s.startBlock(deferExit)
18  s.lastDeferExit = deferExit
19  s.lastDeferCount = len(s.openDefers)
20  zeroval := s.constInt8(types.Types[TUINT8], 0)
21  // 倒序檢查 defer
22  for i := len(s.openDefers) - 1; i >= 0; i-- {
23    r := s.openDefers[i]
24    bCond := s.f.NewBlock(ssa.BlockPlain)
25    bEnd := s.f.NewBlock(ssa.BlockPlain)
26
27    // 檢查 deferBits
28    deferBits := s.variable(&deferBitsVar, types.Types[TUINT8])
29    // 創建 if deferBits & 1 << len(defer) != 0 { ... }
30    bitval := s.constInt8(types.Types[TUINT8], 1<<uint(i))
31    andval := s.newValue2(ssa.OpAnd8, types.Types[TUINT8], deferBits, bitval)
32    eqVal := s.newValue2(ssa.OpEq8, types.Types[TBOOL], andval, zeroval)
33    b := s.endBlock()
34    b.Kind = ssa.BlockIf
35    b.SetControl(eqVal)
36    b.AddEdgeTo(bEnd)
37    b.AddEdgeTo(bCond)
38    bCond.AddEdgeTo(bEnd)
39    s.startBlock(bCond)
40
41    // 如果創建的條件分支被觸發,則清空當前的延遲比特: deferBits &^= 1 << len(defers)
42    nbitval := s.newValue1(ssa.OpCom8, types.Types[TUINT8], bitval)
43    maskedval := s.newValue2(ssa.OpAnd8, types.Types[TUINT8], deferBits, nbitval)
44    s.store(types.Types[TUINT8], s.deferBitsAddr, maskedval)
45    s.vars[&deferBitsVar] = maskedval
46
47    // 處理被延遲的函數調用,取出保存的入口地址、參數信息
48    argStart := Ctxt.FixedFrameSize()
49    fn := r.n.Left
50    stksize := fn.Type.ArgWidth()
51    ...
52    for j, argAddrVal := range r.argVals {
53      f := getParam(r.n, j)
54      pt := types.NewPtr(f.Type)
55      addr := s.constOffPtrSP(pt, argStart+f.Offset)
56      if !canSSAType(f.Type) {
57        s.move(f.Type, addr, argAddrVal)
58      } else {
59        argVal := s.load(f.Type, argAddrVal)
60        s.storeType(f.Type, addr, argVal, 0, false)
61      }
62    }
63    // 調用
64    var call *ssa.Value
65    ...
66    call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, fn.Sym.Linksym(), s.mem())
67    call.AuxInt = stksize
68    s.vars[&memVar] = call
69    ...
70    s.endBlock()
71    s.startBlock(bEnd)
72  }
73}

從整個過程中我們可以看到,開放編碼式 defer 並不是絕對的零成本,儘管編譯器能夠做到將延遲調用直接插入返回語句之前,但出於語義的考慮,需要在棧上對參與延遲調用的參數進行一次求值;同時出於條件語句中可能存在的 defer,還額外需要通過延遲比特來記錄一個延遲語句是否在運行時被設置。因此,開放編碼式 defer 的成本體現在非常少量的指令和位運算來配合在運行時判斷是否存在需要被延遲調用的 defer。

defer 的優化之路defer 的早期實現其實是非常的粗糙的。每當出現一個 defer 調用,都會在堆上分配 defer 記錄,並對參與調用的參數實施一次拷貝操作,然後將其加入到 defer 鍊表上;當函數返回需要觸發 defer 調用時,依次將 defer 從鍊表中取出,完成調用。當然最初的實現並不需要完美,未來總是可以迭代其性能問題。在 Go 1.1 的開發階段,defer 獲得了它的第一次優化 [Cox, 2011]。Russ Cox 意識到 defer 性能問題的根源是當產生多個 defer 調用時,造成的過多的內存分配與拷貝操作,進而提出將 defer 的分配和釋放過程在每個 Goroutine 內進行批量處理。當時 Dmitry Vyukov 則提議在棧上分配會更加有效,但 Russ Cox 錯誤的認為在執行棧上分配 defer 記錄與在其他地方進行分配並沒有帶來太多收益,最終實現了 per-G 批量式分配的 defer 機制。由於後續調度器的改進,工作竊取調度的引入,運行時開始支持 per-P 的局部資源池,defer 作為發生在 Goroutine 內的調用,所需的內存自然也是一類可以被視作局部持有的資源。因此分配和釋放 defer 的資源在 Go 1.3 時得到優化 [Vyukov, 2014],Dmitry Vyukov 將 per-G 分配的 defer 改為了從 per-P 資源池分配的機制。由於分配延遲記錄 _defer 的調用 newdefer 可能存在本地資源池、全局資源池均不存在可復用的內存,進而導致棧分裂,更糟糕的情況下甚至可能發生搶佔,導致 M/P 解綁與綁定等額外的調度開銷。因此,Austin Clements 對 defer 做的一個優化 [Clements, 2016] 是在每個 deferproc 和 deferreturn 中都切換至系統棧,從而阻止了搶佔和棧增長的發生,也就優化消除了搶佔帶來的 M/P 綁定所帶來的開銷。除此之外,對於每次產生記錄時,無論參數大小如何都涉及 memmove 系統調用,從而產生一次 memmove 的調用成本,Austin 的優化中還特地針對沒有參數和指針大小參數的這兩種情況進行了判斷,從而跳過了這些特殊情況下情況下 memmove 帶來的開銷。後來,Keith Randall 終於實現了 [Randall, 2013] 很早之前 Dmitry Vyukov 就已經提出的在棧上分配 defer 的優化 [Cox, 2011],簡單情況下不再需要使用運行時對延遲記錄的內存管理。為 Go 1.13 進一步提升了 defer 的性能。在 Go 1.14 中,Dan Scales 作為 Go 團隊的新成員,defer 的優化成為了他的第一個項目。他提出開放式編碼 defer [Scales, 2019],通過編譯器輔助信息和延遲比特在函數末尾處直接獲取調用函數及參數,完成了近乎零成本的 defer 調用,成為了 Go 1.14 中幾個出色的運行時性能優化之一。

我們最後來總結一下 defer 的基本工作原理以及三種 defer 的性能取捨,如下圖:不同類型 defer 的編譯與運行時成本之間的取捨編譯器會直接將所需的參數進行存儲,並在返回語句的末尾插入被延遲的調用;當整個調用中邏輯上會執行的 defer 不超過 15 個(例如 7 個 defer 作用在 2 個返回語句)、總 defer 數量不超過 8 個、且沒有出現在循環語句中時,會激活使用此類 defer;此類 defer 的唯一的運行時成本就是存儲參與延遲調用的相關信息,運行時性能最好。編譯器會直接在棧上記錄一個 _defer 記錄,該記錄不涉及內存分配,並將其作為參數,傳入被翻譯為 deferprocStack 的延遲語句,在延遲調用的位置將 _defer 壓入 Goroutine 對應的延遲調用鍊表中;在函數末尾處,通過編譯器的配合,在調用被 defer 的函數前,調用 deferreturn,將被延遲的調用出棧並執行;此類 defer 的唯一運行時成本是從 _defer 記錄中將參數複製出,以及從延遲調用記錄鍊表出棧的成本,運行時性能其次。編譯器首先會將延遲語句翻譯為一個 deferproc 調用,進而從運行時分配一個用於記錄被延遲調用的 _defer 記錄,並將被延遲的調用的入口地址及其參數複製保存,入棧到 Goroutine 對應的延遲調用鍊表中;在函數末尾處,通過編譯器的配合,在調用被 defer 的函數前,調用 deferreturn,從而將 _defer 實例歸還到資源池,而後通過模擬尾遞歸的方式來對需要 defer 的函數進行調用。此類 defer 的主要性能問題存在於每個 defer 語句產生記錄時的內存分配,記錄參數和完成調用時的參數移動時的系統調用,運行時性能最差。
進一步閱讀的參考文獻[Griesemer, 2009] Robert Griesemer. defer statement. Jan 27, 2009. https://github.com/golang/go/commit/4a903e0b32be5a590880ceb7379e68790602c29d[Thompson, 2009] Ken Thompson. defer. Jan 27, 2009. https://github.com/golang/go/commit/1e1cc4eb570aa6fec645ff4faf13431847b99db8[Cox, 2011] Russ Cox. runtime: aggregate defer. Oct, 2011. https://github.com/golang/go/issues/2364[Clements, 2016] Austin Clements. runtime: optimize defer code. Sep, 2016. https://github.com/golang/go/commit/4c308188cc05d6c26f2a2eb30631f9a368aaa737[Ma, 2016] Minux Ma. runtime: defer is slow. Mar, 2016. https://github.com/golang/go/issues/14939[Randall, 2013] Keith Randall. cmd/compile: allocate some defers in stack frames. Dec, 2013. https://github.com/golang/go/issues/6980[Vyukov, 2014] Dmitry Vyukov. runtime: per-P defer pool. Jan, 2014. https://github.com/golang/go/commit/1ba04c171a3c3a1ea0e5157e8340b606ec9d8949

[Scales, 2019] Dan Scales, Keith Randall, and Austin Clements. Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case. Sep, 2019. https://go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

相關焦點

  • Go 語言之 defer 的前世今生 - CSDN
    作者 | 歐長坤來源 | 碼農桃花源延遲語句 defer 在最早期的 Go 語言設計中並不存在,後來才單獨增加了這一特性,由 Robert Griesemer 完成語言規範的編寫 [Griesemer, 2009], 並由 Ken Thompson 完成最早期的實現 [Thompson, 2009],兩人合作完成這一語言特性。
  • 深入理解 Go 語言 defer
    Go 語言的 func 聲明中如果返回值變量顯示聲明,也就是  `func foo() (ret int) {} ` 的時候,rval 就是 ret。閉包簡單來說,Go 語言中的閉包就是在函數內引用函數體之外的數據,這樣就會產生一種結果,雖然數據定義是在函數外,但是在函數內部操作數據也會對數據產生影響。如下面的例子所示,foo() 中的匿名函數對 i 的調用就是閉包引用,i++ 會影響外面定義的 i 的值。而 bar() 中的匿名函數是變量拷貝,i++ 並不會修改外部 i 值。
  • Go語言defer你不知道的事
    Go 語言中的 defer 語句用於延遲函數的調用
  • go 學習筆記之解讀什麼是defer延遲函數
    延伸閱讀參考文檔Defer_statements[2]go 語言的 defer 語句[3]Go defer 實現原理剖析>[4]go 語言 defer 你不知道的秘密![5]Go 語言中 defer 的一些坑[6]go defer (go 延遲函數)[7]參考資料[1]Defer
  • Go技術日報(2021-02-16)——解密 defer 原理,究竟背著程序猿做了多少事情?
    go 中文網每日資訊--2021-02-16 一、#公眾號:Go 語言中文網1.Go 語言標準庫中 atomic.Value 的前世今生2.解密 defer 原理,究竟背著程序猿做了多少事情?4.使用 Go 語言編寫的使用 K6 工具進行 Web 應用程式負載測試的簡便方法 [2]5.pdfcpu:Go PDF 處理器 [3]來源:https://gocn.vip/topics/node18gopherDaily--2021-02-16 2.Kubernetes 的 Pod 如何獲得一個
  • Go 經典入門系列 28:Defer
    什麼是 defer? defer 語句的用途是:含有 defer 語句的函數,會在該函數將要返回之前,調用另一個函數。這個定義可能看起來很複雜,我們通過一個示例就很容易明白了。該程序輸出:Welcome John Smith實參取值(Arguments Evaluation) 在 Go 語言中,並非在調用延遲函數的時候才確定實參,而是當執行 defer 語句的時候,就會對延遲函數的實參進行求值。
  • 袁勇麟教授暢談協和的前世今生
    >作為「老協和人」,講座伊始,他以幽默親切的語言講述了自己和協和學院的淵源。《協和的前世今生》專題教育講座 楊競雯/攝談及學院的「今生」,新生入學講座《協和的前世今生》同學認真聽講 劉夢卿/攝新生入學講座《協和的前世今生》 國際商學系同學認真聽講座  謝寧靜/攝
  • 坑爹代碼 | Go 語言的 defer 能製造出多少坑來?
    Go 語言的 defer 語句是一個非常有用的特性,可以將一個方法延遲到包裹該方法的方法返回時執行
  • 前世今生因果輪迴
    世界如此之大無奇不有,我們生活在這美好的世界裡,人生在世是否真的會有前世與今生。每一個人都在猜想,都在找答案。如果真的有前世,就會想到有沒有來世。前世與今生如果真的還有今生,那麼今生無法報答的恩情等到來世再報 。人世間是如此美好,今生修來的福分是前世的因果。好人必有好報。前世的因果,決定了今生的命運。
  • Go 的 defer 的特性還是有必要要了解下的!!!
    defer 會在當前函數返回之前執行 defer 註冊的函數。比如 defer func_x( )  這樣語句會讓你註冊一個函數變量到 defer 的全局鍊表中,在 defer 語句所在的函數退出之前調用。
  • 『GCTT 出品』Go 中 defer 的 5 個坑 - 第一部分
    #2 — 在循環中使用 defer切忌在循環中使用 defer,除非你清楚自己在做什麼,因為它們的執行結果常常會出人意料。但是,在某些情況下,在循環中使用 defer 會相當方便,例如將函數中的遞歸轉交給 defer,但這顯然已經不是本文應該講解的內容。
  • 廖閱鵬:前世今生催眠曲,帶你夢回前世,總結今生!
    最近在最右上,看到了一則消息,許多人聽了廖閱鵬的前世今生催眠曲,都看到了自己的前世,我覺得很神奇,便趁著月黑風高之夜,孤身一人躲在被窩裡,悄悄的打開了喜馬拉雅收音機,點開了前世今生催眠曲,帶上耳機,準備一場穿越之旅。
  • 今生的夫妻是前世情人,今生的情人是前世夫妻:善待每一份相遇!
    作者:胡楊映月情人之所以對你柔情似水,之所以是浪漫溫柔的代名詞,之所以讓你感覺愛得百轉柔腸,之所以讓你刻骨銘心,是因為你們是前世的夫妻。今生之所以尋你而來,只因為前世的一份緣還沒有盡,所以今生來續前緣,是來還債的。
  • 60分鐘快速了解Go語言
    發明Go語言是出於更好地完成工作的需要。Go不是計算機科學的最新發展潮流,但它卻提供了解決現實問題的最新最快的方法。Go擁有命令式語言的靜態類型,編譯很快,執行也很快,同時加入了對於目前多核CPU的並發計算支持,也有相應的特性來實現大規模編程。Go語言有非常棒的標準庫,還有一個充滿熱情的社區。
  • 假設檢驗的前世今生
    其實,「前世今生」系列的文章我已經看到過好幾篇了,比如「正太分布的前世今生」、「Meta分析的前世今生」。不知為何,我個人也很喜歡「前世今生」這個詞。今天呢,就聊一聊我知道的一點「假設檢驗的前世今生」吧。 假設檢驗是統計學裡最重要、最基礎的的概念,即便是不知道,不了解這個術語,與統計學毫不相干的人,在日常生活中,也不知不覺地應用了假設檢驗。
  • 使用 Panic、Defer 和 Recover 處理 Go 錯誤
    原文如下:這篇文章主要會與大家介紹 Go 語言的錯誤處理。我們將會討論關於 Go 語言創建和捕獲自定義、運行時錯誤的一些簡單方法。Go 提供了簡單方法實現。Go 提供了簡單的錯誤接口,每個返回錯誤都必須實現這個接口。
  • Golang中defer的實現原理
    前言在Go語言中,可以使用關鍵字defer向函數註冊退出調用,即主函數退出時,defer後的函數才被調用。defer語句的作用是不管程序是否出現異常,均在函數退出時自動執行相關代碼。如下圖所示:defer的創建與執行我們先來看一下彙編是如何翻譯defer關鍵字的 0x0082 00130 (test.go:16) CALL runtime.deferproc(SB) 0x0087 00135 (test.go:16) TESTL AX, AX
  • 今生的愛人就是前世埋葬你的人
    用感恩之心去愛自己的另一半,用奉獻之心去澆灌愛之花朵,用心去經營自己愛的城堡——家庭,那麼,幸福將跟隨你一生一世!1、有個這樣的故事古時有位窮苦的書生,和未婚妻早已定下婚約。好不容易盼到了自己的大喜之日,未婚妻卻嫁給了別人。書生承受不了這種打擊,從此一病不起。在一次拜佛之時,請求佛祖為他指點迷津,於是,佛祖把他帶回了前世。
  • 小兒推拿的前世今生(前世篇下)
    小兒推拿的前世今生(前世篇上) 上一期我們講了小兒推拿的史料積累期,那麼當資料積累到一定程度,那麼就會交叉混合產生出新的學科。今天我們就來了解一下小兒推拿的形成期。這個時期主要從明清時代開始。小兒推拿根基的建立與素材隨著按摩知識的積累,人們已經發現小兒的生理病理現象可以通過在一定穴位或部位上點按與撫觸來調節;已經歸納出手法的「開達」與「抑遏」之性;已經形成了「按之則熱氣至」、「按之則血氣散」、「按之痛止」、「按而收之」、「推而散之」等關於按摩的理論;
  • 人人值得一看——談前世 | 贈書《前世今生》
    佛在經上常說「欲知前世因,今生受者是」,你要知道這前世的因果,你看看現在受的就是;「欲知來世果,今生作者是」,想想來世什麼樣的果報,現在造的因就是。 你看看現在這個世界,這果!你就曉得前世造什麼樣的因;再看看現在社會大家造的因,我就曉得將來社會會有什麼果報。歷史是一面鏡子!這決定不是假的。