Go言語Internal パート2:名前付き戻り値の魅力的な利点

ご承知のことと思いますが、Go言語では戻り値に名前を付ける機能を提供しています。これまでminioではこの機能をそれほど使っていませんが、この先変わって行くでしょう。というのも、本記事で以下に説明するような魅力的な利点が含まれているからです。

私たちのような開発者であれば、下記に示すように “デフォルト”値を返すためにreturn文ごとに新しいオブジェクトのインスタンスを生成するという形で大量のコードを使うことがあるでしょう。

type objectInfo struct {
    arg1 int64
    arg2 uint64
    arg3 string
    arg4 []int
}
func NoNamedReturnParams(i int) (objectInfo) {

    if i == 1 {
        // Do one thing
        return objectInfo{}
    }

    if i == 2 {
        // Do another thing
        return objectInfo{}
    }

    if i == 3 {
        // Do one more thing still
        return objectInfo{}
    }

    // Normal return
    return objectInfo{}
}

Go言語のコンパイラが生成する実際のコードを見ると、次のようになります。

"".NoNamedReturnParams t=1 size=243 args=0x40 locals=0x0
0x0000  TEXT    "".NoNamedReturnParams(SB), $0-64
0x0000  MOVQ    $0, "".~r1+16(FP)
0x0009  LEAQ    "".~r1+24(FP), DI
0x000e  XORPS   X0, X0
0x0011  ADDQ    $-16, DI
0x0015  DUFFZERO    $288
0x0028  MOVQ    "".i+8(FP), AX
0x002d  CMPQ    AX, $1
0x0031  JEQ $0, 199
0x0037  CMPQ    AX, $2
0x003b  JEQ $0, 155
0x003d  CMPQ    AX, $3
0x0041  JNE 111
0x0043  MOVQ    "".statictmp_2(SB), AX
0x004a  MOVQ    AX, "".~r1+16(FP)
0x004f  LEAQ    "".~r1+24(FP), DI
0x0054  LEAQ    "".statictmp_2+8(SB), SI
0x005b  DUFFCOPY    $854
0x006e  RET
0x006f  MOVQ    "".statictmp_3(SB), AX
0x0076  MOVQ    AX, "".~r1+16(FP)
0x007b  LEAQ    "".~r1+24(FP), DI
0x0080  LEAQ    "".statictmp_3+8(SB), SI
0x0087  DUFFCOPY    $854
0x009a  RET
0x009b  MOVQ    "".statictmp_1(SB), AX
0x00a2  MOVQ    AX, "".~r1+16(FP)
0x00a7  LEAQ    "".~r1+24(FP), DI
0x00ac  LEAQ    "".statictmp_1+8(SB), SI
0x00b3  DUFFCOPY    $854
0x00c6  RET
0x00c7  MOVQ    "".statictmp_0(SB), AX
0x00ce  MOVQ    AX, "".~r1+16(FP)
0x00d3  LEAQ    "".~r1+24(FP), DI
0x00d8  LEAQ    "".statictmp_0+8(SB), SI
0x00df  DUFFCOPY    $854
0x00f2  RET

すばらしく見えますが、少々繰り返しが多いように見えるなら、まさにその通りです。要するに、return文ごとに、返されるオブジェクトが多かれ少なかれ割り当て済みのものだったり、初期化済のものだったり(あるいは、もっと正確に言うとDUFFCOPYマクロによるコピー)します。

結局、これが、あらゆる場合においてreturn objectInfo{}を使った戻しによって要求していることです。

戻り値に名前を付ける

では、簡単な変更を加えたらどうなるか見てみましょう。つまり、戻り値に名前(oi)を付け、Go言語による”引数なし”の戻り機能を使います。(return文から引数を取り除きます。ただし、必ずというわけではありません。詳しくは後ほど述べます。)

func NamedReturnParams(i int) (oi objectInfo) {

    if i == 1 {
        // Do one thing
        return
    }

    if i == 2 {
        // Do another thing
        return
    }

    if i == 3 {
        // Do one more thing still
        return
    }

    // Normal return
    return
}

さらにコンパイラが生成したコードでは、次のようになります。

"".NamedReturnParams t=1 size=67 args=0x40 locals=0x0
    0x0000  TEXT    "".NamedReturnParams(SB), $0-64
    0x0000  MOVQ    $0, "".oi+16(FP)
    0x0009  LEAQ    "".oi+24(FP), DI
    0x000e  XORPS   X0, X0
    0x0011  ADDQ    $-16, DI
    0x0015  DUFFZERO    $288
    0x0028  MOVQ    "".i+8(FP), AX
    0x002d  CMPQ    AX, $1
    0x0031  JEQ $0, 66
    0x0033  CMPQ    AX, $2
    0x0037  JEQ $0, 65
    0x0039  CMPQ    AX, $3
    0x003d  JNE 64
    0x003f  RET
    0x0040  RET
    0x0041  RET
    0x0042  RET

全部で4つあるオブジェクト初期化の出現の仕方、DUFFCOPYが消滅という極めて大きな違いがあります(しかもこんな小さな例でもです)。関数の大きさが243バイトから67バイトに減っています。さらに利点があり、コードの終了時点でのCPUのサイクルも節約できます。というのも、戻り値のセットアップをする必要がないからです。

もし、Go言語が提供する引数なしの戻りを好まなかったり選びたくなかったりする場合は、return oiを使って、同様の利点を得ることができます。それは以下のようなものです。

    if i == 1 {
        return oi
    }

Minioサーバでの実例

Minioサーバでもう少し調べてみると、次のような事例が見つかりました。

// parse credentialHeader string into its structured form.
func parseCredentialHeader(credElement string) (credentialHeader) {
    creds := strings.Split(strings.TrimSpace(credElement), "=")
    if len(creds) != 2 {
    return credentialHeader{}
    }
    if creds[0] != "Credential" {
    return credentialHeader{}
    }
    credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
    if len(credElements) != 5 {
    return credentialHeader{}
    }
    if false /*!isAccessKeyValid(credElements[0])*/ {
    return credentialHeader{}
    }
    // Save access key id.
    cred := credentialHeader{
    accessKey: credElements[0],
    }
    var e error
    cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
    if e != nil {
    return credentialHeader{}
    }
    cred.scope.region = credElements[2]
    if credElements[3] != "s3" {
    return credentialHeader{}
    }
    cred.scope.service = credElements[3]
    if credElements[4] != "aws4_request" {
    return credentialHeader{}
    }
    cred.scope.request = credElements[4]
    return cred
}

アセンブリを見てみると、以下のような関数ヘッダが得られます(詳細なリストはやめておこうと思います)。

"".parseCredentialHeader t=1 size=1157 args=0x68 locals=0xb8

コードを変更して、名前付き戻りパラメータ(下のソースコードの2番目のブロック)を使うと、関数の大きさに何が起こるのか調べてみてください。

"".parseCredentialHeader t=1 size=863 args=0x68 locals=0xb8

合計1150バイトから300バイトも減っていて、ソースコードへの最小限の変更の結果としては悪くはありません。また、あなたの考え次第で、”より簡潔な”ソースコードの体裁を選んでも構いません。

// parse credentialHeader string into its structured form.
func parseCredentialHeader(credElement string) (ch credentialHeader) {
    creds := strings.Split(strings.TrimSpace(credElement), "=")
    if len(creds) != 2 {
    return
    }
    if creds[0] != "Credential" {
    return
    }
    credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
    if len(credElements) != 5 {
    return
    }
    if false /*!isAccessKeyValid(credElements[0])*/ {
    return
    }
    // Save access key id.
    cred := credentialHeader{
    accessKey: credElements[0],
    }
    var e error
    cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
    if e != nil {
    return
    }
    cred.scope.region = credElements[2]
    if credElements[3] != "s3" {
    return
    }
    cred.scope.service = credElements[3]
    if credElements[4] != "aws4_request" {
    return
    }
    cred.scope.request = credElements[4]
    return cred
} 

実際にはch変数は、関数の中で定義される他のローカル変数と全く同じように通常のローカル変数であることに注意してください。したがって、その値をデフォルトの”ゼロ”の状態から変更することができます(もちろんその場合、終了時には変更されたバージョンが返されます)。

名前付き戻り値のその他の使い方

数人に指摘されているように、名前付き戻り値のその他の利点は、クロージャ(例えばdefer文)で使用することです。このように、defer文の結果として呼び出される関数の名前付き戻り値にアクセスし、それに応じて動作しても構いません。

連載について

この連載のパート1を見逃したなら、以下のリンクをご覧ください

まとめ

このように、次第に、新しいコードだけではなく既存のコードにも、名前付き戻り値がますます採用されていくことになるでしょう。実際、このプロセスを支援したり自動化したりするためのちょっとしたユーティリティを開発できるかどうかについても調べているところです。上で述べた変更を施すために、gofmtの方針に従いつつソースを自動で変更することを考えてみてください。特に、戻り値にまだ名前が付いていない場合(だからユーティリティは値に名前を与えなければなりません)、必ずしもこの戻り変数が既存のソース内で何らかの方法で変更される場合である必要はありません。このように、(上に挙げた事例で)return chを用いても、プログラムの機能的変化は一切生じません。では今後の動向にご期待ください。

この記事が有益なものとなり、Goが内部でどのように動作し、Go言語のコードをいかに改善していくかということについて、何らかの新しい知見を与えられれば幸いです。

更新

上で述べたような状況がよくなる場合に同一のコードを生成させるため、Go言語がコンパイラを最適化するというissueが作成されました。