1. ホーム
  2. スクリプト・コラム
  3. ゴラン

Go deferの原理とソースコード分解(推奨)

2022-02-07 21:33:59

Go言語には非常に便利な予約語deferがあり、これをラップした関数が戻るまで実行が延期される関数を呼び出すことができます。

は、その

defer文が関数を呼び出すのは、それをラップしている関数がreturn文を実行して関数本体の末尾に到達するか、対応するgoroutineがパニックに陥るかのどちらかです。

実際のgoプログラムでは、他の言語のtry...catch...の代わりにdefer文が使われたり、ファイルハンドルのクローズ、データベース接続のクローズなど、リソースの解放などのクローズ操作を処理するために使われたりする。

1. コンパイラはdefer手続きをコンパイルする

ドーゾメッシング(x)を延期する

つまり、defer文を実行すると、後で実行する関数を実際に登録し、関数名と引数を決定しますが、すぐに呼び出すのではなく、現在の関数が戻るかパニックが発生するまで呼び出しを延期するのです。

まず、deferに関連するデータ構造を理解することから始めましょう。

1) 構造体 _defer のデータ構造

go言語プログラムの中でdeferを呼び出すと、それぞれ_defer構造体が生成されます。

type _defer struct {
    siz int32 // memory size of parameters and return values
    started boul
    heap boul // distinguishes whether the structure is allocated on the stack or on the pair
    sp uintptr // sp counter value, stack pointer.
    pc uintptr // pc counter value, program counter.
    fn *funcval // address of the function passed in by defer, i.e., the function whose execution is deferred;
    _panic *_panic // panic that is running defer
    link *_defer // link table
}

デフォルトでgo 1.13のソースコードを使用していますが、他のバージョンも同様です。

1つの関数内に複数の defer 呼び出しがある場合があるので、当然、これらの _defer 構造体を整理するためのデータ構造が必要です。この _defer は、アライメント規則に従って 48 バイトのメモリを使用します。defer構造体のリンクフィールドは、すべての_deferをチェーンにつなぎますが、Goroutineにぶら下がる_deferフィールドが先頭となります。

のチェーン構造は

deferの連鎖構造は以下の通りです。

_defer.sizは、遅延された関数の引数と戻り値のためのスペースを指定するために使用されます。そのサイズは、_defer.sizによって指定されます。このメモリの値は、defer キーワードが実行されたときに満たされます。

遅延関数の引数は事前に計算され、スタック上にスペースが確保されます。deferの呼び出しごとにスタック上に確保されるメモリのレイアウトを下図に示す。

ここで、_deferはstruct _deferオブジェクトへのポインターで、スタックまたはヒープ上に確保されます。

2) 構造体_deferのメモリ割り当て

以下はdeferの使用例で、ファイル名はtest_defer.goです。

package main

func doDeferFunc(x int) {
    println(x)
}

func doSomething() int {
    var x = 1
    defer doDeferFunc(x)
    x += 2
    return x
}

func main() {
    x := doSomething()
    println(x)
}

上記のコードをコンパイルし、remove optimization と inline linking オプションを追加します。

go ツール コンパイル -N -l test_defer.go

アセンブリコードをエクスポートします。

ゴーツール objdump test_defer.o

コンパイルされたバイナリコードを見てみましょう。

アセンブリ命令から、コンパイラはdeferキーワードに遭遇したときに、いくつかのランタイムライブラリ関数を追加することがわかります。 deferprocStack deferreturn .

正式リリースされた go 1.13 では defer のパフォーマンスが改善され、defer シナリオで 30% のパフォーマンス改善を謳っています。

1.13より前のバージョンのgoのdefer文は、コンパイラによって2つの手続きに変換されていました。 Callback registration function procedure: deferproc deferreturn

.

go 1.13 では、この 30% の性能向上の核となる deferprocStack 関数が導入されました。deferprocStack と deferproc はどちらもコールバック関数を登録するためのものですが、deferprocStatck は struct _defer 構造体をスタックメモリ上に確保し、一方 deferproc はヒープ上に構造体メモリを確保しなければならない点が異なっています。私たちのシナリオの大部分はスタック上に割り当てることができるので、当然、全体のパフォーマンスが向上します。スタックへのメモリ割り当ては、当然ながらペアよりもはるかに高速で、rsp レジスタの値を変更するだけで実行できます。

では、どのような場合にスタックに割り当て、どのような場合にヒープに割り当てるのでしょうか。

コンパイラ関連ファイル(src/cmd/compile/internal/gc/ssa.go )に、条件判断で、以下のようなものがある。

func (s *state) stmt(n *Node) {
 
    case ODEFER:
        d := callDefer
        if n.Esc == EscNever {
            d = callDeferStack
        }
}

n.Escはast.Nodeのエスケープ解析の結果なので、n.EscがEscNeverになるのはいつ頃なのでしょうか?

{n.Esc これはエスケープ解析関数esc(src/cmd/compile/internal/gc/esc.go)内にあります。

func (e *EscState) esc(n *Node, parent *Node) {

    case ODEFER:
        if e.loopdepth == 1 { // top level
            n.Esc = EscNever // force stack allocation of defer record (see ssa.go)
            break
        }
}

ここでは、e.loopdepth が 1 のときだけ EscNever に設定されています。e.loopdepth フィールドは、ネストされたループ スコープを検出するために使用されます。言い換えると、次のように、ネストされたスコープのコンテキストにある場合、defer によって struct _defer がヒープ上に割り当てられる可能性があります。

package main

func main() {
    for i := 0; i < 10; i++ {
        defer func() {
            _ = i
        }()
    }
}

コンパイラはdeferprocを生成します。

deferの外層が明示的(for)または暗黙的(goto)の場合、struct _defer構造体がヒープ上に確保され、パフォーマンスが悪くなるので、プログラミングする際は注意が必要です。

コンパイラは、_defer 構造体をスタックに割り当てるか、ヒープに割り当てるかを決定することができます。対応する関数は deferprocStatck 関数と deferproc 関数で、どちらも単純で目的は同じです: _defer 構造体のメモリ構造を割り当て、その中でコールバック関数を初期化し、チェーンテーブルにフックします。

3) deferprocStack スタック上への割り当て

deferprocStack関数は何をするのですか?

// The memory structure is already allocated on the stack before entering this function
func deferprocStack(d *_defer) {
    gp := getg()

    // siz and fn are assigned before entering this function
    d.started = false
    // indicates that it is the memory of the stack
    d.heap = false
    // Get the rsp register value of the caller function and assign it to the sp field of the _defer structure
    d.sp = getcallersp()
    // Get the rip register value of the caller function and assign it to the pc field of the _defer structure
    // Based on the principle of function calls, we know that the pc (rip) value of the caller's stack is the next instruction in the deferprocStack
    d.pc = getcallerpc()

    // hook this _defer structure into the goroutine's chain as a node
    *(*uintptr)(unsafe.Pointer(&d._panic)) = 0
    *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
    *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
    // Note that the special return, which does not trigger a delayed call to the function
    return0()
}

概要を説明します。

  • メモリはスタック上に割り当てられるため、コンパイラは deferprocStack の呼び出しの前に struct _defer 構造体の関数を準備します。
  • _defer.heap フィールドは、この構造体がスタック上に割り当てられていることを識別するために使用されます。
  • コンテキストを保存し、呼び出し元関数の rsp、pc (rip) レジスタの値を _defer 構造体に保存します。
  • デファーはチェーンテーブルのノードとしてペグされます。注:テーブルヘッダはgoroutine構造体の_deferフィールドで、並行タスクではそのほとんどが複数の関数呼び出しを持っているので、このチェーンは呼び出し側スタックの_defer構造体にリンクされ、実行は区別するためにrspに従ってフィルタリングされます; 4) deferprocヒープ割り当て

ヒープを確保する関数はdeferprocで、以下のような簡略化されたロジックで行われます。

func deferproc(siz int32, fn *funcval) {
  // arguments of fn fullow fn
    // Get the rsp register value of the caller function
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    // Get the pc(rip) register value of the caller function
    callerpc := getcallerpc()

    // allocate struct _defer memory structure
    d := newdefer(siz)
    if d._panic ! = nil {
        throw("deferproc: d.panic ! = nil after newdefer")
    }
    // _defer structure initialization
    d.fn = fn
    d.pc = callerpc
    d.sp = sp
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }
    // Note that the special return, which does not trigger a delayed call to the function
    return0()
}

概要を説明します。

  • オンスタックアロケーションとは異なり、struct _defer 構造体はこの関数で割り当てられ、newdefer 関数では、まず poul キャッシュプールを探して、あれば直接受け取り、なければ mallocgc を呼び出してヒープからメモリを確保します。
  • deferproc は、遅延関数の引数と戻り値のメモリサイズをそれぞれ特定する siz, fn というエントリと、遅延関数のアドレスを受け取ります。
  • ヒープ上に割り当てられた構造体を識別するための _defer.heap フィールド。
  • コンテキストを保存し、呼び出し元関数の rsp, pc (rip) レジスタの値を _defer 構造体に保存します。
  • チェーンテーブルにペグされたノードとしての_defer。

5) defer 機能の連鎖を実行する

コンパイラは defer 文に遭遇し、2つの関数を挿入します。

  • 代入関数:deferproc または deferprocStack。
  • 関数を実行する: deferreturn .

defer文をラップしている関数が終了するとき、deferreturnはすべての遅延されたコールチェーンを実行する責任を負います。

func deferreturn(arg0 uintptr) {
    gp := getg()
    // Get the top _defer node
    d := gp._defer
    // function recursion termination condition (d chain table traversal complete)
    if d == nil {
        return
    }
    // Get the rsp register value of the caller function
    sp := getcallersp()
    if d.sp ! = sp {
        // if _defer.sp does not match the caller's sp value, then return it directly.
        // Because of this, it means that the _defer structure is not registered with the caller function  
        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))
    }
    // Get the address of the delayed callback function
    fn := d.fn
    d.fn = nil
    // remove the current _defer node from the chain
    gp._defer = d.link
    // free the _defer memory (mainly the heap will need to handle it, the stack is reclaimed as the function finishes executing and the stack shrinks)
    freedefer(d)
    // execute delayed callback function
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

コードの説明です。

  • defer chain を繰り返し実行し、chain が空になるまで、前から後ろへ順次 chain を一つずつ pick off します。
  • jmpdeferは、遅延コールバック関数にジャンプして命令を実行し、実行完了後にdeferreturnにジャンプバックする役割を担っています。
  • _defer.spの値から、現在の呼び出し元関数が登録しているものを判断し、自関数が登録した遅延コールバック関数だけを実行するようにすることができる。

例えば、a() -> b() -> c()で、aがbを呼び出し、bがcを呼び出し、a、b、cの3つの関数すべてにデファラが登録されていれば、当然cのコールバックはc()関数が戻ったときに実行されます。

2. deferは引数を渡す

1) 事前に計算されたパラメータ

先ほどの_deferのデータ構造を説明する際に、メモリ構造が以下のようになっていることを述べました。

はヘッダーとしてスタック上に配置され、遅延コールバック関数(defer)の引数と戻り値は_deferの直後に配置され、deferが実行されるときに設定される、つまり、defer関数が実行されるまで待つのではなく、引数は事前に計算されている。

例えば、defer func(x, y)を実行すると、2つの実パラメータxとyが計算されますが、Goの関数呼び出しはバリューパッシング(値渡し)です。そして、xとyの値が_defer構造体にコピーされます。別の例を見てください。

package main

func main() {
    var x = 1
    defer println(x)
    x += 2
    return
}

このプログラムの出力は何でしょうか?1でしょうか、それとも3でしょうか?答えは1です。deferは関数printlnを実行し、printlnの引数はxで、xの値はdefer文で確認しながら渡します。

2) deferのための引数を用意する

defer関数の引数は、すでに_deferで連続したメモリブロックに格納されています。では、defer関数が実行されたとき、引数はどこから来るのでしょうか?もちろん、直接_deferのアドレスに来るわけではありません。なぜなら、これは標準的な関数呼び出しだからです。

Goでは、関数の引数は呼び出し側の関数が用意します。例えば、main() -> A(7) -> B(a) のようにスタックフレームを形成しています。

つまり、deferreturnはdefer関数ディレクティブにジャンプする以外にもうひとつ、defer delayコールバック関数に必要な引数(スペースと値)を準備する必要があるのです。そして、次のようなコードで照準が行われるのです。

func deferreturn(arg0 uintptr) {

    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(uns1) The process of calling the function

To understand this process, it is first necessary to know the process of function invocation: the

  • a line of function call statements in go is actually a non-atomic operation, corresponding to multiple lines of assembly instructions, including 1) parameter setting and 2) call instruction execution.
  • where the call assembly instruction also has two elements: the return address stack (which causes the rsp value to grow downward, rsp-0x8), and the callee function address loaded into the pc register.
  • go's one-line function return statement is actually a non-atomic operation, corresponding to multiple lines of assembly instructions, including 1) return value setting and 2) ret instruction execution.
  • where the contents of the ret assembly instruction are two, the instruction pc register is restored to the address saved at the top of the rsp stack, and rsp is scaled up, rsp+0x8.
  • where the arguments are set in the caller function and the return value is set in the callee function.
  • the two registers rsp, rbp are the two most important registers of the stack frame, the values of which delimit the stack frame.

The most important point: Go's return call is a compound operation that can correspond to the following two sequences of operations.

  • Set the return value
  • The ret instruction jumps to the caller function

2) Does the return return the value first or does the defer function execute first?

The official Golang documentation clearly states that

That is, if the surrounding function returns through an explicit return statement, deferred functions are executedafter any result parameters are set by that return statementbutbefore the function returns to its caller.

That is, defer's function chain call is made after the return value is set but before the run instruction context returns to the caller function.

So the function containing the defer registration, after the return statement is executed, corresponds to the execution of three sequences of operations.

  • Set the return value
  • Execute the defer chain
  • The ret instruction jumps to the caller function

So, based on this principle, let's parse the following behavior.

func f1 () (r int) {
    t := 1
    defer func() {
        t = t + 5
    }()
    return t
}

func f2() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1
}

func f3() (r int) {
    defer func () {
        r = r + 5
    } ()
    return 1
}

What is the return value of each of these three functions?

Answer: f1() -> 1, f2() -> 1, f3() -> 6 .

a) After the function f1 executes the return t statement.

  • sets the return value r = t, when the value of the local variable t is equal to 1, so r = 1.
  • execute the defer function with t = t+5, after which the value of the local variable t is 6.
  • executes the assembly ret instruction, which jumps to the caller function.

So, the return value of f1() is 1; the

b) After the f2 function executes the return 1 statement.

  • set the return value r = t, when the value of the local variable t is equal to 1, so r = 1.
  • execute the defer function with t = t+5, after which the value of the local variable t is 6.
  • executes the assembly ret instruction, which jumps to the caller function.

So, the return value of f2() is still 1; the

c) After the f3 function executes the return 1 statement.

  • sets the return value r = 1.
  • execute the defer function with r = r+5, after which the return value of the variable r is 6 (this is a closure function, note the distinction from f2).
  • executes the assembly ret instruction, which jumps to the caller function.

So, the return value of f1() is 6.

  • the defer keyword implementation corresponds to the _defer data structure, which was always allocated on the heap during go1.1 - go1.12, and was optimized to allocate the _defer structure on the stack after go1.13, with significant performance improvements.
  • _defer is allocated on the stack in most scenarios, but will be allocated on the heap in loop nesting scenarios, so pay attention to defer usage scenarios when programming, otherwise there may be performance problems.
  • _defer corresponds to a registered deferred callback function (defer), the arguments and return value of the defer function follow _defer, which can be interpreted as the header, _defer and the memory where the function arguments and return value are located is a contiguous space, where _defer.siz specifies the size of the space occupied by the arguments and return value.
  • functions registered by defer in the same concatenation, all hanging in a chain table with goroutine._defer as the header.

The new element is inserted at the top, and the iteration is executed from the top to the bottom. So defer-registered functions have the LIFO characteristic, i.e., the later-registered ones are executed first.

different functions are on this chain, distinguished by _defer.sp.

The arguments to defer are pre-calculated, that is, they are confirmed when the defer keyword is executed, and the assignment is behind the memory block of _defer. when executed, copied to the corresponding location on the stack frame.

return corresponds to a compound operation with 3 actions: set return value, execute defer function chain, and ret instruction jump.

Reference. Programming Library go Language Tutorial .

This article about the Go defer principle and source code analysis is introduced here, more related Go defer principle content please search the previous articles of the Codedevlib or continue to browse the following related articles hope you support the Codedevlib more in the future!