2017年8月17日
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 が作成されました。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa