概要
- 英語記事: Golang Internals Part 1: Autogenerated functions (and how to get rid of them)
- 原文公開日: 2017/06/15
- 著者: Frank Wessels
Go言語の内部(1)自動生成関数とその抑制方法(翻訳)
皆さんもMinioでの私たちと同様、この頃Go言語のコールスタックでちょくちょく「自動生成」関数に出くわしては、これが一体何なのか気になっているのではないかと思います。
以前私たちが遭遇した事例では、次のようなコールスタックが出力されました。
cmd.retryStorage.ListDir(0x12847c0, 0xc420402e70, 0x1, ...)
minio/cmd/retry-storage.go:183 +0x72
cmd.(*retryStorage).ListDir(0xc4201624b0, 0xdf3b5f, 0xe, 0x25, ...)
:932 +0xaf
cmd.cleanupDir.func1(0xf18f1ec540, 0x25, 0x24, 0xde7eb5)
minio/cmd/object-api-com.go:215 +0xe1
cmd.cleanupDir(0x1284860, 0xc4201624b0, 0xdf3b5f, ...)
minio/cmd/object-api-com.go:231 +0x1d1
ご覧のとおり、2番目の関数は1番目の関数(値レシーバ、Minioのminio/cmd/retry-storage.go:183
で定義)とよく似ていますが、ポインタレシーバを受け取っている点が異なります。もうひとつ疑わしいのは、2番目の関数には行番号だけが表示され、ソースコードのファイル名が空欄になっている点です。
訳注: Go言語では、レシーバを取る関数を「メソッド」と呼びますが、本記事の原文ではメソッドという呼び方をしていません。
このコールスタックを生成したコードの出どころは、以下の関数です。この関数ではstorage.ListDir()
を呼び出す(再帰的)関数が宣言されており、呼び出しによってcleanupDir()
にstorage
が渡されます。
// ディレクトリを再帰的にクリーンアップする
func cleanupDir(storage StorageAPI, volume, dirPath string) error {
var delFunc func(string) error
// エントリを再帰的に削除する関数
delFunc = func(entryPath string) error {
// ディレクトリのリストを表示
entries, err := storage.ListDir(volume, entryPath)
if err != nil {
return err
}
// 再帰して他のエントリをすべて削除
for _, entry := range entries {
err = delFunc(pathJoin(entryPath, entry))
if err != nil {
return err
}
}
return nil
}
return delFunc(retainSlash(pathJoin(dirPath)))
}
上のコードを見た人は、storage.ListDir
(値レシーバ)は、途中でポインタレシーバ版(cmd.(*retryStorage).ListDir
)を呼び出す必要なしに、即座に呼び出されると仮定するかもしれません。では実際にはどうなるのでしょうか?
自動生成された関数
最初に、このポインタレシーバ関数が一体何なのかを調べてみましょう。go tool objdump -s ListDir minio
を実行してこの関数の定義を取得してみると、「自動生成された」ようです。
TEXT minio/cmd.(*retryStorage).ListDir(SB) <自動生成>
<autogenerated>:926 0x1f2e00 GS MOVQ GS:0x8a0, CX
<autogenerated>:926 0x1f2e09 CMPQ 0x10(CX), SP
<autogenerated>:926 0x1f2e0d JBE 0x1f2f42
<autogenerated>:926 0x1f2e13 SUBQ $0x78, SP
<autogenerated>:926 0x1f2e17 MOVQ BP, 0x70(SP)
<autogenerated>:926 0x1f2e1c LEAQ 0x70(SP), BP
<autogenerated>:926 0x1f2e21 MOVQ 0x20(CX), BX
<autogenerated>:926 0x1f2e25 TESTQ BX, BX
<autogenerated>:926 0x1f2e28 JE 0x1f2e3a
<autogenerated>:926 0x1f2e2a LEAQ 0x80(SP), DI
<autogenerated>:926 0x1f2e32 CMPQ DI, 0(BX)
<autogenerated>:926 0x1f2e35 JNE 0x1f2e3a
<autogenerated>:926 0x1f2e37 MOVQ SP, 0(BX)
<autogenerated>:926 0x1f2e3a MOVQ 0x80(SP), AX
<autogenerated>:926 0x1f2e42 TESTQ AX, AX
<autogenerated>:926 0x1f2e45 JE 0x1f2efa
<autogenerated>:926 0x1f2e4b MOVQ 0x80(SP), AX
<autogenerated>:926 0x1f2e53 MOVQ 0(AX), CX
<autogenerated>:926 0x1f2e56 MOVQ CX, 0(SP)
<autogenerated>:926 0x1f2e5a LEAQ 0x8(AX), SI
<autogenerated>:926 0x1f2e5e LEAQ 0x8(SP), DI
<autogenerated>:926 0x1f2e63 MOVQ BP, -0x10(SP)
<autogenerated>:926 0x1f2e68 LEAQ -0x10(SP), BP
<autogenerated>:926 0x1f2e6d CALL 0x5d5a4
<autogenerated>:926 0x1f2e72 MOVQ 0(BP), BP
<autogenerated>:926 0x1f2e76 MOVQ 0x88(SP), AX
<autogenerated>:926 0x1f2e7e MOVQ AX, 0x28(SP)
<autogenerated>:926 0x1f2e83 MOVQ 0x90(SP), AX
<autogenerated>:926 0x1f2e8b MOVQ AX, 0x30(SP)
<autogenerated>:926 0x1f2e90 MOVQ 0x98(SP), AX
<autogenerated>:926 0x1f2e98 MOVQ AX, 0x38(SP)
<autogenerated>:926 0x1f2e9d MOVQ 0xa0(SP), AX
<autogenerated>:926 0x1f2ea5 MOVQ AX, 0x40(SP)
<autogenerated>:926 0x1f2eaa CALL cmd.retryStorage.ListDir(SB)
<autogenerated>:926 0x1f2eaf MOVQ 0x48(SP), AX
<autogenerated>:926 0x1f2eb4 MOVQ 0x50(SP), CX
<autogenerated>:926 0x1f2eb9 MOVQ 0x58(SP), DX
<autogenerated>:926 0x1f2ebe MOVQ 0x60(SP), BX
<autogenerated>:926 0x1f2ec3 MOVQ 0x68(SP), SI
<autogenerated>:926 0x1f2ec8 MOVQ AX, 0xa8(SP)
<autogenerated>:926 0x1f2ed0 MOVQ CX, 0xb0(SP)
<autogenerated>:926 0x1f2ed8 MOVQ DX, 0xb8(SP)
<autogenerated>:926 0x1f2ee0 MOVQ BX, 0xc0(SP)
<autogenerated>:926 0x1f2ee8 MOVQ SI, 0xc8(SP)
<autogenerated>:926 0x1f2ef0 MOVQ 0x70(SP), BP
<autogenerated>:926 0x1f2ef5 ADDQ $0x78, SP
<autogenerated>:926 0x1f2ef9 RET
<autogenerated>:926 0x1f2efa LEAQ 0x96c2bb(IP), AX
<autogenerated>:926 0x1f2f01 MOVQ AX, 0(SP)
<autogenerated>:926 0x1f2f05 MOVQ $0x3, 0x8(SP)
<autogenerated>:926 0x1f2f0e LEAQ 0x976870(IP), AX
<autogenerated>:926 0x1f2f15 MOVQ AX, 0x10(SP)
<autogenerated>:926 0x1f2f1a MOVQ $0xc, 0x18(SP)
<autogenerated>:926 0x1f2f23 LEAQ 0x9707a2(IP), AX
<autogenerated>:926 0x1f2f2a MOVQ AX, 0x20(SP)
<autogenerated>:926 0x1f2f2f MOVQ $0x7, 0x28(SP)
<autogenerated>:926 0x1f2f38 CALL runtime.panicwrap(SB)
<autogenerated>:926 0x1f2f3d JMP 0x1f2e4b
<autogenerated>:926 0x1f2f42 CALL runtime.morestack_noctxt(SB)
<autogenerated>:926 0x1f2f47 JMP cmd.(*retryStorage).ListDir(SB)
真ん中あたりのオフセットが0x1f2eaa
の行でretryStorage.ListDir
(値レシーバ)を呼び出しているのがわかります。この関数はminio/cmd/retry-storage.go:183
にあるGo言語のコードで定義されています。そこより上の行ではスタックにすべての引数を設定しており、その中で *retryStorage
の参照を解決してそのコピーをスタック上に作成しています(retryStorage.ListDir
で必要になるため)。
そのCALL
の次では戻り引数(return arguments)が読み込まれ(オフセット0x1f2eaf
から0x1f2ec3
までの行)、適切なスロットにコピーされます(オフセット0x1f2ec8
から0x1f2ee8
までの行)。これは、この自動生成された関数から戻った後で(*retryStorage).ListDir
の戻り引数になります。
つまり、(*retryStorage).ListDir
が行っているのは、本質的にはretryStorage.ListDir
呼び出しをラップして、ポインタのレシーバが指すオブジェクトが変更されないようにすることだけです。
Go言語がこのようにする理由
(*retryStorage).ListDir
の動作については理解できました。次の疑問は、Go言語がなぜこんなことをするのかです。その理由は、StorageAPI
がインターフェイス型であるという事実と関連しなければなりません。すなわち、cleanupDir()
のstorage
引数の値はインターフェイスであり、実質的に2つのポインタで構成されます。インターフェイスの2つの値は、そのインターフェイスに保存される型に関する情報へのポインタと、それに関連付けられるデータを指すポインタを与える2つのワードのペアで表されます。
したがって、storage.ListDir()
を呼び出すと、retryStorage()
を指すポインタ(ただし値レシーバメソッド版のListDir()
のみ)にアクセスできる状態になってしまいます。ここでGo言語コンパイラは手回しよく(自動)生成を行いますが、オブジェクトの参照を解決して戻り引数などをコピーしなければならない分かなり余分なコストを伴います。この点を明らかにしてくれた@thatcksのご指摘に感謝いたします。
自動生成を止めたいときは?
最初に申し上げたいのは、「修正」は必ずしも必要ではないという点です。本質的に間違ったことは行われておらず、問題なくコードを実行できるからです。
しかしどうしても修正したいのであれば、嘘のように簡単な方法で行えます。minio/cmd/retry-storage.go:183
のfunc (f retryStorage) ListDir(...)
を変更してfunc (f *retryStorage) ListDir(...)
にするだけで次の結果を得られます。
TEXT github.com/minio/minio/cmd.(*retryStorage).ListDir(SB)
retry-storage.go:199 0x15edc0 GS MOVQ GS:0x8a0, CX
retry-storage.go:199 0x15edc9 CMPQ 0x10(CX), SP
retry-storage.go:199 0x15edcd JBE 0x15efe5
<以下省略>
きっとあなたは、この関数の実装が誤ってf
の値を変更することのないようにしたくなると思います。いずれその可能性に気づくでしょう(たぶんこのあたりは少々気持ち悪いかもしれません: これは値レシーバ関数では発生せず、おそらく呼び出し側はf
の内容を変更しようとする意図に気づくでしょう)。
もうひとつのフリーボーナスは、実行ファイルのサイズを数100バイトも削減できることです(この関数ひとつでそうなるのですから、ソースコードファイルに*
をひとつ足すだけで相当いい感じになりそうです)。
まとめ
本記事が、Go言語の関数の内部動作に関する何らかの洞察となり、こうした自動生成関数の正体や、それに対して何ができるかを明らかにできれば幸いです。
本シリーズでは今後、ポインタレシーバ関数と値レシーバ関数のパフォーマンスについてもっと詳しく書きます。
Nitish TiwariとHarshavardhanaに感謝いたします。