defer は Go 言語の非常に興味深いキーワード機能です。例は次のとおりです:
package main import "fmt" func main() { defer fmt.Println("煎鱼了") fmt.Println("脑子进") }
出力結果は次のとおりです:
脑子进 煎鱼了
数日前、私の読者グループの友人数人が次の問題について議論しました:
簡単に言えば、for ループで 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), BPGo1.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 キーワードはパフォーマンスに影響を与えますか?」
- 显式循环:在调用 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教程栏目!