Goの名前付き戻り値
2020-12-11
こんにちは
どうも僕です。
この記事は[KITアドベントカレンダー11日目](https://qiita.com/advent-calendar/2020/kitdev)の記事になります。
今回はGolangの名前付き戻り値について簡単にまとめてみました。
そもそも名前付き戻り値って何?
まずこれですよね、僕も最初はわからんってなってましたし、今もよくわかってないです。
簡単にいうと、戻り値に名前をつけておくことができます。比較は以下のコードで。
// 通常の関数
func nomalFunc(name string) string {
return "hello " + name
}
// 名前付き戻り値
func nomalFunc(name string) (result string) { // 戻り値に名前をつけてる
result = "hello " + name
return
}
こんな感じでreturnが呼ばれたときには予め決めた名前を返すことができます。
このコードでは恩恵が感じられませんが、エラーハンドリングとかするときで、複数のエラーが転がってる時などにこれが役に立ちます。さらにはメモリの使用量まで減ります。
コーディングで大規模リクエストを捌く時などはここらへんの工夫も重要になってくるということです。
検証してみる
検証したみたいと思います。
多分名前付き戻り値の方が速いんだろうなっていう想像はつくんじゃないかなと思います(いちいちオブジェクトを返す手間がなくなるので)
まず、今回は構造体を使用していきたいと思います。
type Human struct {
Name string
Age int
Alcohol []string
}
var Me = Human{
Name: "takurinton",
Age: 21,
Alcohol: []string{"beer", "cassis orange"},
}
こんな感じ、ビールとカシオレが好きです。まじで。
次にそれぞれの関数の定義をしてみたいと思います。
今回はif文を作りたかったので雑に条件分岐してます。ちなみに意味はない。
if文に関してはあってもなくてもパフォーマンスに変化はあります。
通常の関数
こんな感じで実装していきます。
func main() {
human := Me
fmt.Println(NoReturnNamedMeInfo(human))
}
func NoReturnNamedMeInfo(human Human) Human {
if human.Age < 20 {
return human
}
if human.Age > 20 && human.Age < 40 {
return human
}
if human.Age > 40 && human.Age < 60 {
return human
}
return human
}
通常の関数ではHumanという戻り値の型だけを簡単に定義しました。
また、雑な条件分岐を入れてあります。
ここでGolangのアセンブラコードをデバッグしてみます。
実行コマンドは
go tool comlie -S filename.go
です。これでいい感じに出てくれます。
"".NoReturnNamedMeInfo STEXT nosplit size=173 args=0x60 locals=0x0
0x0000 00000 (nomal_function.go:24) TEXT "".NoReturnNamedMeInfo(SB), NOSPLIT|ABIInternal, $0-96
0x0000 00000 (nomal_function.go:24) PCDATA $0, $-2
0x0000 00000 (nomal_function.go:24) PCDATA $1, $-2
0x0000 00000 (nomal_function.go:24) FUNCDATA $0, gclocals·05818c00683dfd2dc6b701de434c1854(SB)
0x0000 00000 (nomal_function.go:24) FUNCDATA $1, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x0000 00000 (nomal_function.go:24) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (nomal_function.go:24) PCDATA $0, $0
0x0000 00000 (nomal_function.go:24) PCDATA $1, $0
0x0000 00000 (nomal_function.go:24) XORPS X0, X0
0x0003 00003 (nomal_function.go:24) MOVUPS X0, "".~r1+56(SP)
0x0008 00008 (nomal_function.go:24) MOVUPS X0, "".~r1+72(SP)
0x000d 00013 (nomal_function.go:24) MOVUPS X0, "".~r1+88(SP)
0x0012 00018 (nomal_function.go:25) MOVQ "".human+24(SP), AX
0x0017 00023 (nomal_function.go:25) CMPQ AX, $20
0x001b 00027 (nomal_function.go:25) JLT 142
0x001d 00029 (nomal_function.go:28) PCDATA $0, $-1
0x001d 00029 (nomal_function.go:28) PCDATA $1, $-1
0x001d 00029 (nomal_function.go:28) JLE 37
0x001f 00031 (nomal_function.go:28) PCDATA $0, $0
0x001f 00031 (nomal_function.go:28) PCDATA $1, $0
0x001f 00031 (nomal_function.go:28) CMPQ AX, $40
0x0023 00035 (nomal_function.go:28) JLT 111
0x0025 00037 (nomal_function.go:31) CMPQ AX, $40
0x0029 00041 (nomal_function.go:31) JLE 49
0x002b 00043 (nomal_function.go:31) CMPQ AX, $60
0x002f 00047 (nomal_function.go:31) JLT 80
0x0031 00049 (nomal_function.go:35) PCDATA $1, $1
0x0031 00049 (nomal_function.go:35) MOVUPS "".human+8(SP), X0
0x0036 00054 (nomal_function.go:35) MOVUPS X0, "".~r1+56(SP)
0x003b 00059 (nomal_function.go:35) MOVUPS "".human+24(SP), X0
0x0040 00064 (nomal_function.go:35) MOVUPS X0, "".~r1+72(SP)
0x0045 00069 (nomal_function.go:35) PCDATA $1, $2
0x0045 00069 (nomal_function.go:35) MOVUPS "".human+40(SP), X0
0x004a 00074 (nomal_function.go:35) MOVUPS X0, "".~r1+88(SP)
0x004f 00079 (nomal_function.go:35) RET
0x0050 00080 (nomal_function.go:32) PCDATA $1, $1
0x0050 00080 (nomal_function.go:32) MOVUPS "".human+8(SP), X0
0x0055 00085 (nomal_function.go:32) MOVUPS X0, "".~r1+56(SP)
0x005a 00090 (nomal_function.go:32) MOVUPS "".human+24(SP), X0
0x005f 00095 (nomal_function.go:32) MOVUPS X0, "".~r1+72(SP)
0x0064 00100 (nomal_function.go:32) PCDATA $1, $2
0x0064 00100 (nomal_function.go:32) MOVUPS "".human+40(SP), X0
0x0069 00105 (nomal_function.go:32) MOVUPS X0, "".~r1+88(SP)
0x006e 00110 (nomal_function.go:32) RET
0x006f 00111 (nomal_function.go:29) PCDATA $1, $1
0x006f 00111 (nomal_function.go:29) MOVUPS "".human+8(SP), X0
0x0074 00116 (nomal_function.go:29) MOVUPS X0, "".~r1+56(SP)
0x0079 00121 (nomal_function.go:29) MOVUPS "".human+24(SP), X0
0x007e 00126 (nomal_function.go:29) MOVUPS X0, "".~r1+72(SP)
0x0083 00131 (nomal_function.go:29) PCDATA $1, $2
0x0083 00131 (nomal_function.go:29) MOVUPS "".human+40(SP), X0
0x0088 00136 (nomal_function.go:29) MOVUPS X0, "".~r1+88(SP)
0x008d 00141 (nomal_function.go:29) RET
0x008e 00142 (nomal_function.go:26) PCDATA $1, $1
0x008e 00142 (nomal_function.go:26) MOVUPS "".human+8(SP), X0
0x0093 00147 (nomal_function.go:26) MOVUPS X0, "".~r1+56(SP)
0x0098 00152 (nomal_function.go:26) MOVUPS "".human+24(SP), X0
0x009d 00157 (nomal_function.go:26) MOVUPS X0, "".~r1+72(SP)
0x00a2 00162 (nomal_function.go:26) PCDATA $1, $2
0x00a2 00162 (nomal_function.go:26) MOVUPS "".human+40(SP), X0
0x00a7 00167 (nomal_function.go:26) MOVUPS X0, "".~r1+88(SP)
0x00ac 00172 (nomal_function.go:26) RET
0x0000 0f 57 c0 0f 11 44 24 38 0f 11 44 24 48 0f 11 44 .W...D$8..D$H..D
0x0010 24 58 48 8b 44 24 18 48 83 f8 14 7c 71 7e 06 48 $XH.D$.H...|q~.H
0x0020 83 f8 28 7c 4a 48 83 f8 28 7e 06 48 83 f8 3c 7c ..(|JH..(~.H..<|
0x0030 1f 0f 10 44 24 08 0f 11 44 24 38 0f 10 44 24 18 ...D$...D$8..D$.
0x0040 0f 11 44 24 48 0f 10 44 24 28 0f 11 44 24 58 c3 ..D$H..D$(..D$X.
0x0050 0f 10 44 24 08 0f 11 44 24 38 0f 10 44 24 18 0f ..D$...D$8..D$..
0x0060 11 44 24 48 0f 10 44 24 28 0f 11 44 24 58 c3 0f .D$H..D$(..D$X..
0x0070 10 44 24 08 0f 11 44 24 38 0f 10 44 24 18 0f 11 .D$...D$8..D$...
0x0080 44 24 48 0f 10 44 24 28 0f 11 44 24 58 c3 0f 10 D$H..D$(..D$X...
0x0090 44 24 08 0f 11 44 24 38 0f 10 44 24 18 0f 11 44 D$...D$8..D$...D
0x00a0 24 48 0f 10 44 24 28 0f 11 44 24 58 c3 $H..D$(..D$X.
実行したらこんな感じになりました。
MOVUPSを呼び出す回数が多いなという印象を持ちました。
また、`nosplit size=173` というところに注目してください。
これは関数の大きさを表しています。173がどれくらいなのかわからないので次の名前付き戻り値の関数と比較します。
名前付き戻り値を使用した関数
次にこっちです。
名前月の戻り値はreturnの後ろに明示的に定義する必要がないから速くなりそう。
func main() {
human := Me
fmt.Println(NoReturnNamedMeInfo(human))
}
func ReturnNamedMeInfo(human Human) (h Human) {
h = human
if human.Age < 20 {
return
}
if human.Age > 20 && human.Age < 40 {
return
}
if human.Age > 40 && human.Age < 60 {
return
}
return
}
こちらではHumanという型のhという変数を作成して、条件のたびに戻しています。
いい感じ。
次に先ほどと同じようにアセンブリされたコードを見てみましょう。
"".ReturnNamedMeInfo STEXT nosplit size=83 args=0x60 locals=0x0
0x0000 00000 (return_named_value.go:24) TEXT "".ReturnNamedMeInfo(SB), NOSPLIT|ABIInternal, $0-96
0x0000 00000 (return_named_value.go:24) PCDATA $0, $-2
0x0000 00000 (return_named_value.go:24) PCDATA $1, $-2
0x0000 00000 (return_named_value.go:24) FUNCDATA $0, gclocals·05818c00683dfd2dc6b701de434c1854(SB)
0x0000 00000 (return_named_value.go:24) FUNCDATA $1, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x0000 00000 (return_named_value.go:24) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (return_named_value.go:24) PCDATA $0, $0
0x0000 00000 (return_named_value.go:24) PCDATA $1, $0
0x0000 00000 (return_named_value.go:24) XORPS X0, X0
0x0003 00003 (return_named_value.go:24) MOVUPS X0, "".h+56(SP)
0x0008 00008 (return_named_value.go:24) MOVUPS X0, "".h+72(SP)
0x000d 00013 (return_named_value.go:24) MOVUPS X0, "".h+88(SP)
0x0012 00018 (return_named_value.go:25) PCDATA $1, $1
0x0012 00018 (return_named_value.go:25) MOVUPS "".human+8(SP), X0
0x0017 00023 (return_named_value.go:25) MOVUPS X0, "".h+56(SP)
0x001c 00028 (return_named_value.go:25) MOVUPS "".human+24(SP), X0
0x0021 00033 (return_named_value.go:25) MOVUPS X0, "".h+72(SP)
0x0026 00038 (return_named_value.go:25) MOVUPS "".human+40(SP), X0
0x002b 00043 (return_named_value.go:25) MOVUPS X0, "".h+88(SP)
0x0030 00048 (return_named_value.go:26) PCDATA $1, $2
0x0030 00048 (return_named_value.go:26) MOVQ "".human+24(SP), AX
0x0035 00053 (return_named_value.go:26) CMPQ AX, $20
0x0039 00057 (return_named_value.go:26) JLT 82
0x003b 00059 (return_named_value.go:29) PCDATA $0, $-1
0x003b 00059 (return_named_value.go:29) PCDATA $1, $-1
0x003b 00059 (return_named_value.go:29) JLE 67
0x003d 00061 (return_named_value.go:29) PCDATA $0, $0
0x003d 00061 (return_named_value.go:29) PCDATA $1, $2
0x003d 00061 (return_named_value.go:29) CMPQ AX, $40
0x0041 00065 (return_named_value.go:29) JLT 81
0x0043 00067 (return_named_value.go:32) CMPQ AX, $40
0x0047 00071 (return_named_value.go:32) JLE 79
0x0049 00073 (return_named_value.go:32) CMPQ AX, $60
0x004d 00077 (return_named_value.go:32) JLT 80
0x004f 00079 (return_named_value.go:36) PCDATA $0, $-1
0x004f 00079 (return_named_value.go:36) PCDATA $1, $-1
0x004f 00079 (return_named_value.go:36) RET
0x0050 00080 (return_named_value.go:33) RET
0x0051 00081 (return_named_value.go:30) RET
0x0052 00082 (return_named_value.go:27) RET
0x0000 0f 57 c0 0f 11 44 24 38 0f 11 44 24 48 0f 11 44 .W...D$8..D$H..D
0x0010 24 58 0f 10 44 24 08 0f 11 44 24 38 0f 10 44 24 $X..D$...D$8..D$
0x0020 18 0f 11 44 24 48 0f 10 44 24 28 0f 11 44 24 58 ...D$H..D$(..D$X
0x0030 48 8b 44 24 18 48 83 f8 14 7c 17 7e 06 48 83 f8 H.D$.H...|.~.H..
0x0040 28 7c 0e 48 83 f8 28 7e 06 48 83 f8 3c 7c 01 c3 (|.H..(~.H..<|..
0x0050 c3 c3 c3
さっきよりも明らかに短い!MOVUPSが呼ばれてる回数が圧倒的に少ないため、メモリを拡張して変数を格納してるというよりは、現状の変数を参照して返すというような仕様になっているようです。
また、関数のサイズも先ほどの173と比較して83と小さくなりました。このサイズの関数でこれだけ差が出るんだからもっと大きい関数ではもっと差が大きくなりそうで面白そう。
もう少し拡張
もう少し掘ってみてみたいと思います。
次はポインタを使用してアクセスしてみます。
ポインタを使用することでメモリに直接アクセスすることができ、一般的には速度が向上します。
先ほどの関数を少し書き換えて実行してみます。
通常の関数
var Me = &Human{
Name: "takurinton",
Age: 21,
Alcohol: []string{"beer", "cassis orange"},
}
func main() {
human := Me
fmt.Println(NoReturnNamedMeInfoUsePointer(human))
}
func NoReturnNamedMeInfoUsePointer(human *Human) *Human {
if human.Age < 20 {
return human
}
if human.Age > 20 && human.Age < 40 {
return human
}
if human.Age > 40 && human.Age < 60 {
return human
}
return human
}
Meをポインタ代入にしてからHumanをポインタ参照にします。これをすることで速度が高速化する(はず)
"".NoReturnNamedMeInfoUsePointer STEXT nosplit size=59 args=0x10 locals=0x0
0x0000 00000 (nomal_function.go:38) TEXT "".NoReturnNamedMeInfoUsePointer(SB), NOSPLIT|ABIInternal, $0-16
0x0000 00000 (nomal_function.go:38) PCDATA $0, $-2
0x0000 00000 (nomal_function.go:38) PCDATA $1, $-2
0x0000 00000 (nomal_function.go:38) FUNCDATA $0, gclocals·62420d0a7277934df9079483e4a3e39b(SB)
0x0000 00000 (nomal_function.go:38) FUNCDATA $1, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x0000 00000 (nomal_function.go:38) FUNCDATA $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x0000 00000 (nomal_function.go:39) PCDATA $0, $1
0x0000 00000 (nomal_function.go:39) PCDATA $1, $1
0x0000 00000 (nomal_function.go:39) MOVQ "".human+8(SP), AX
0x0005 00005 (nomal_function.go:39) MOVQ 16(AX), CX
0x0009 00009 (nomal_function.go:39) CMPQ CX, $20
0x000d 00013 (nomal_function.go:39) JLT 53
0x000f 00015 (nomal_function.go:42) PCDATA $0, $-1
0x000f 00015 (nomal_function.go:42) PCDATA $1, $-1
0x000f 00015 (nomal_function.go:42) JLE 23
0x0011 00017 (nomal_function.go:42) PCDATA $0, $1
0x0011 00017 (nomal_function.go:42) PCDATA $1, $1
0x0011 00017 (nomal_function.go:42) CMPQ CX, $40
0x0015 00021 (nomal_function.go:42) JLT 47
0x0017 00023 (nomal_function.go:45) CMPQ CX, $40
0x001b 00027 (nomal_function.go:45) JLE 35
0x001d 00029 (nomal_function.go:45) CMPQ CX, $60
0x0021 00033 (nomal_function.go:45) JLT 41
0x0023 00035 (nomal_function.go:49) PCDATA $0, $0
0x0023 00035 (nomal_function.go:49) PCDATA $1, $2
0x0023 00035 (nomal_function.go:49) MOVQ AX, "".~r1+16(SP)
0x0028 00040 (nomal_function.go:49) RET
0x0029 00041 (nomal_function.go:46) MOVQ AX, "".~r1+16(SP)
0x002e 00046 (nomal_function.go:46) RET
0x002f 00047 (nomal_function.go:43) MOVQ AX, "".~r1+16(SP)
0x0034 00052 (nomal_function.go:43) RET
0x0035 00053 (nomal_function.go:40) MOVQ AX, "".~r1+16(SP)
0x003a 00058 (nomal_function.go:40) RET
0x0000 48 8b 44 24 08 48 8b 48 10 48 83 f9 14 7c 26 7e H.D$.H.H.H...|&~
0x0010 06 48 83 f9 28 7c 18 48 83 f9 28 7e 06 48 83 f9 .H..(|.H..(~.H..
0x0020 3c 7c 06 48 89 44 24 10 c3 48 89 44 24 10 c3 48 <|.H.D$..H.D$..H
0x0030 89 44 24 10 c3 48 89 44 24 10 c3
sizeが59にまで減りました!名前付き戻り値を使うよりも速い!これは素晴らしい成果です!
先ほどは変数の代入や宣言によってメモリを都度拡張していましたが、今回はメモリを直接参照しているため増やす必要がなくサイズを抑えることができました。これは知見。
名前付き戻り値
次に名前付き戻り値について見てみます。
先ほどと同じように関数を書き換えます。
func main() {
human := Me
fmt.Println(ReturnNamedMeInfoUsePointer(human))
}
func ReturnNamedMeInfoUsePointer(human *Human) (h *Human) {
h = human
if human.Age < 20 {
return
}
if human.Age > 20 && human.Age < 40 {
return
}
if human.Age > 40 && human.Age < 60 {
return
}
return
}
こんな感じです。先ほどと同じMeへの代入は省略しています。
次に先ほどと同じようにログを見てみます。
"".ReturnNamedMeInfoUsePointer STEXT nosplit size=59 args=0x10 locals=0x0
0x0000 00000 (return_named_value.go:39) TEXT "".ReturnNamedMeInfoUsePointer(SB), NOSPLIT|ABIInternal, $0-16
0x0000 00000 (return_named_value.go:39) PCDATA $0, $-2
0x0000 00000 (return_named_value.go:39) PCDATA $1, $-2
0x0000 00000 (return_named_value.go:39) FUNCDATA $0, gclocals·62420d0a7277934df9079483e4a3e39b(SB)
0x0000 00000 (return_named_value.go:39) FUNCDATA $1, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x0000 00000 (return_named_value.go:39) FUNCDATA $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x0000 00000 (return_named_value.go:41) PCDATA $0, $1
0x0000 00000 (return_named_value.go:41) PCDATA $1, $1
0x0000 00000 (return_named_value.go:41) MOVQ "".human+8(SP), AX
0x0005 00005 (return_named_value.go:41) MOVQ 16(AX), CX
0x0009 00009 (return_named_value.go:41) CMPQ CX, $20
0x000d 00013 (return_named_value.go:41) JLT 53
0x000f 00015 (return_named_value.go:44) PCDATA $0, $-1
0x000f 00015 (return_named_value.go:44) PCDATA $1, $-1
0x000f 00015 (return_named_value.go:44) JLE 23
0x0011 00017 (return_named_value.go:44) PCDATA $0, $1
0x0011 00017 (return_named_value.go:44) PCDATA $1, $1
0x0011 00017 (return_named_value.go:44) CMPQ CX, $40
0x0015 00021 (return_named_value.go:44) JLT 47
0x0017 00023 (return_named_value.go:47) CMPQ CX, $40
0x001b 00027 (return_named_value.go:47) JLE 35
0x001d 00029 (return_named_value.go:47) CMPQ CX, $60
0x0021 00033 (return_named_value.go:47) JLT 41
0x0023 00035 (return_named_value.go:51) PCDATA $0, $0
0x0023 00035 (return_named_value.go:51) PCDATA $1, $2
0x0023 00035 (return_named_value.go:51) MOVQ AX, "".h+16(SP)
0x0028 00040 (return_named_value.go:51) RET
0x0029 00041 (return_named_value.go:48) MOVQ AX, "".h+16(SP)
0x002e 00046 (return_named_value.go:48) RET
0x002f 00047 (return_named_value.go:45) MOVQ AX, "".h+16(SP)
0x0034 00052 (return_named_value.go:45) RET
0x0035 00053 (return_named_value.go:42) MOVQ AX, "".h+16(SP)
0x003a 00058 (return_named_value.go:42) RET
0x0000 48 8b 44 24 08 48 8b 48 10 48 83 f9 14 7c 26 7e H.D$.H.H.H...|&~
0x0010 06 48 83 f9 28 7c 18 48 83 f9 28 7e 06 48 83 f9 .H..(|.H..(~.H..
0x0020 3c 7c 06 48 89 44 24 10 c3 48 89 44 24 10 c3 48 <|.H.D$..H.D$..H
0x0030 89 44 24 10 c3 48 89 44 24 10 c3 .D$..H.D$..
おっと!ポインタを参照して実行した時はメモリのサイズが同じになりました!!これも知見!
ログも同じようになっています。このことから、ポインタで参照すると名前付きだろうがそうじゃなかろうが参照する方法は変わらないということです。
知らんかった。
結局何?
結局これはどのようにして使うのでしょうか。
これに関しては上で示したとおりで、ポインタを使用して参照している場合はそのまま使えばいいでしょう。しかし、値を参照する場合などは名前付き戻り値を積極的に使用していった方がいいかと思います。
まとめ
今回はGolangの名前付き戻り値について考えてみました。意外と知らない人が多かったり、メモリの変化が大きかったり、とても面白いなと感じました。
他の関数やJSONのライブラリなどでも試してみたいと思います。
最後まで読んでいただきありがとうございました!