ホームページ  >  記事  >  バックエンド開発  >  Go defer を使用する場合は、次の 2 つの点に注意してください。

Go defer を使用する場合は、次の 2 つの点に注意してください。

藏色散人
藏色散人転載
2021-07-10 15:09:282174ブラウズ

defer は Go 言語の非常に興味深いキーワード機能です。例は次のとおりです:

package main

import "fmt"

func main() {
    defer fmt.Println("煎鱼了")

    fmt.Println("脑子进")
}

出力結果は次のとおりです:

脑子进
煎鱼了

数日前、私の読者グループの友人数人が次の問題について議論しました:

Go defer を使用する場合は、次の 2 つの点に注意してください。

簡単に言えば、for ループで defer キーワードを使用した場合、パフォーマンスに影響があるかどうかが問題です。

Go 言語の基礎となるデータ構造の設計では、defer はリンク リスト データ構造であるためです。

Go defer を使用する場合は、次の 2 つの点に注意してください。

誰もが、ループがが大きすぎると、遅延リンク リストが巨大になります。「優れている」とは言えません。それとも、Go defer の設計が Redis データ構造の設計に似ていて、私自身最適化したものの、実際には大きな影響がないのではないかと疑問に思っていますか?

今日の記事では、Go defer ループについて説明します。基礎となるリンク リストが長すぎる場合、問題が発生しますか? 発生する場合、具体的にはどのような影響がありますか?

魚を引き寄せる旅を始めましょう。

遅延パフォーマンスの最適化を 30% 改善

Go1.13 の初期に、遅延に関するパフォーマンスの最適化ラウンドを実施しました。これにより、ほとんどのシナリオで遅延パフォーマンスが 30% 改善されました。

Go defer を使用する場合は、次の 2 つの点に注意してください。

Go1.13 の変更点を確認して、Go defer が最適化された箇所を見てみましょう。これが問題の重要なポイントです。

以前と現在の比較

Go1.12 以前では、Go defer を呼び出すときのアセンブリ コードは次のとおりです。

    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)
    0x0075 00117 (main.go:6)    TESTL    AX, AX
    0x0077 00119 (main.go:6)    JNE    137
    0x0079 00121 (main.go:7)    XCHGL    AX, AX
    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP
Go1.13 以降では、 Go defer を呼び出すときのアセンブリ コード コードは次のとおりです。

    0x006e 00110 (main.go:4)    MOVQ    AX, (SP)
    0x0072 00114 (main.go:4)    CALL    runtime.deferprocStack(SB)
    0x0077 00119 (main.go:4)    TESTL    AX, AX
    0x0079 00121 (main.go:4)    JNE    139
    0x007b 00123 (main.go:7)    XCHGL    AX, AX
    0x007c 00124 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x0081 00129 (main.go:7)    MOVQ    112(SP), BP
アセンブリの観点から見ると、元の

runtime.deferproc の呼び出し方法が、 のメソッドに変更されているようです。 runtime.deferprocStack を呼び出しています。これは完了しましたか?どのような最適化ですか?

私たちは

疑問を抱いたままとして読み続けます。

defer 最小単位: _defer

以前のバージョンと比較して、Go defer

_defer 構造の最小単位は主に heap フィールド: を追加します。

type _defer struct {
    siz     int32
    siz     int32 // includes both arguments and results
    started bool
    heap    bool
    sp      uintptr // sp at time of defer
    pc      uintptr
    fn      *funcval
    ...
このフィールドは、この

_defer がヒープに割り当てられているかスタックに割り当てられているかを識別するために使用されます。他のフィールドは明確に変更されていないため、##defer# のスタックに焦点を当てることができます。 ## が割り当てられているので、何が行われるかを見てみましょう。 deferprocStack

func deferprocStack(d *_defer) {
    gp := getg()
    if gp.m.curg != gp {
        throw("defer on system stack")
    }
    
    d.started = false
    d.heap = false
    d.sp = getcallersp()
    d.pc = getcallerpc()

    *(*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))

    return0()
}
このコード部分は非常に従来型のもので、主に呼び出し元の

defer

関数の関数スタック ポインタ、関数に渡されるパラメータの特定のアドレスを取得します。関数とPC(プログラムカウンタ)については、以前の記事「Go Deferを徹底理解する」で詳しく紹介していますので、ここでは詳しく説明しません。

この deferprocStack

の何が特別なのでしょうか?

d.heap

false に設定されていることがわかります。これは、deferprocStack メソッドが _defer## 用であることを意味します。 # スタック上に割り当てられたアプリケーション シナリオ。 deferproc問題は、ヒープ上に割り当てられたアプリケーション シナリオをどこで処理するかということです。

func newdefer(siz int32) *_defer {
    ...
    d.heap = true
    d.link = gp._defer
    gp._defer = d
    return d
}

特定の

newdefer

は、次のように呼び出される場所です。

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
    ...
    sp := getcallersp()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()

    d := newdefer(siz)
    ...
}

前の手順で deferproc メソッドが呼び出されたことは明らかです。バージョン。ヒープ上に割り当てられたシナリオに対応するために使用されるようになりました。

概要

確かなことは、

deferproc

は削除されておらず、プロセスが最適化されていることです。
  • Go コンパイラは、アプリケーション シナリオに応じて deferproc メソッドまたは
  • deferprocStack
  • メソッドの使用を選択します。これらはそれぞれ、ヒープとスタックへの割り当ての使用シナリオに対応しています。 。 最適化はどこにありますか?
  • 主な最適化は、遅延オブジェクトのスタック割り当てルールの変更にあります。対策は次のとおりです:
コンパイラの ## の

#defer

#for-loop

分析のために深く反復します。

// src/cmd/compile/internal/gc/esc.go
case ODEFER:
    if e.loopdepth == 1 { // top level
        n.Esc = EscNever // force stack allocation of defer record (see ssa.go)
        break
    }
Go コンパイラは、ループの深さ (loop Depth) が 1 であることを検出すると、エスケープ解析の結果を設定してスタックに割り当てられます。それ以外の場合は、ヒープに割り当てられます。
// src/cmd/compile/internal/gc/ssa.go
case ODEFER:
    d := callDefer
    if n.Esc == EscNever {
        d = callDeferStack
    }
    s.call(n.Left, d)
これにより、これまでの systemstack、

mallocgc

、およびその他のメソッドの頻繁な呼び出しによって引き起こされていた大量のパフォーマンス オーバーヘッドが排除され、ほとんどのシナリオでパフォーマンスが向上します。

ループ呼び出し遅延遅延最適化の原理を理解した後、問題自体に戻ります。次に、「ループ内の defer キーワードはパフォーマンスに影響を与えますか?」

最も直接的な影響は、パフォーマンスの最適化の約 30% が完全に失われることであり、姿勢が正しくないため、理論的には defer にオーバーヘッドが発生します (リンクリストが長くなり)も大きくなり、パフォーマンスが低下します。

したがって、次の 2 つのシナリオを回避する必要があります:

  • 显式循环:在调用 defer 关键字的外层有显式的循环调用,例如:for-loop 语句等。
  • 隐式循环:在调用 defer 关键字有类似循环嵌套的逻辑,例如:goto 语句等。

显式循环

第一个例子是直接在代码的 for 循环中使用 defer 关键字:

func main() {
    for i := 0; i <p>这个也是最常见的模式,无论是写爬虫时,又或是 Goroutine 调用时,不少人都喜欢这么写。</p><p>这属于显式的调用了循环。</p><h3>隐式循环</h3><p>第二个例子是在代码中使用类似 <code>goto</code> 关键字:</p><pre class="brush:php;toolbar:false">func main() {
    i := 1
food:
    defer func() {}()
    if i == 1 {
        i -= 1
        goto food
    }
}

这种写法比较少见,因为 goto 关键字有时候甚至会被列为代码规范不给使用,主要是会造成一些滥用,所以大多数就选择其实方式实现逻辑。

这属于隐式的调用,造成了类循环的作用。

总结

显然,Defer 在设计上并没有说做的特别的奇妙。他主要是根据实际的一些应用场景进行了优化,达到了较好的性能。

虽然本身 defer 会带一点点开销,但并没有想象中那么的不堪使用。除非你 defer 所在的代码是需要频繁执行的代码,才需要考虑去做优化。

否则没有必要过度纠结,在实际上,猜测或遇到性能问题时,看看 PProf 的分析,看看 defer 是不是在相应的 hot path 之中,再进行合理优化就好。

所谓的优化,可能也只是去掉 defer 而采用手动执行,并不复杂。在编码时避免踩到 defer 的显式和隐式循环这 2 个雷区就可以达到性能最大化了。

更多golang相关技术文章,请访问golang教程栏目!

以上がGo defer を使用する場合は、次の 2 つの点に注意してください。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。