Tech Racho エンジニアの「?」を「!」に。
  • 開発

Go言語の内部(1)自動生成関数とその抑制方法(翻訳)

概要

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:183func (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 TiwariHarshavardhanaに感謝いたします。

関連記事

Goby: Rubyライクな言語(1)Gobyを動かしてみる


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。