現在Udemyで現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発を受講しています。講義の中でスライスのmakeとcapacityの内容があり、スライスと配列についての挙動が気になったのでまとめました。
はじめに
受講している内容
現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
講師の酒井潤さんについて
シリエン戦隊JUN TVでYouTuberとして活動もされています。シリコンバレーのエンジニア事情などとても面白いです。 www.youtube.com
Splunkで働いている現役シリコンバレーエンジニアの方の講義を手軽に受けられるのは魅力的です。講義の内容や話のテンポも無駄な事を削ぎ落としていると感じたので、私にはとても心地良く感じました。シリコンバレーの風景なども動画におさめられているのはとても面白いと思いました。私はもはやファンになってしまっているので自身の学習内容にマッチしているのであれば、私は酒井潤さん推しです。気になる方は是非チェックしてみて下さい。
実践していく
講義から学んだGoの配列とスライスについては14〜16のレクチャーが該当となり、動画で言うと10分ほどです。プログラムは以下に公開しています。講義内容には影響ないように考慮しています。
※リポジトリ内のreflect
のケースは当記事では未記載
配列について
宣言方法
以下のような形で宣言する事が可能です。
var array1 [2]int array2 := [3]int{1, 2, 3} array3 := [...]int{1, 2, 3, 4}
配列の挙動を試す
以下のようにして、配列内の長さ、容量、値、アドレスを見てみます。
var array1 [2]int fmt.Printf("array1 => length=%d capacity=%d value=%v address=%p \n", len(array1), cap(array1), array1, &array1) array2 := [4]int{} fmt.Printf("array2 => length=%d capacity=%d value=%v address=%p \n", len(array2), cap(array2), array2, &array2) array3 := [4]int{1, 2, 3} fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3) array3[3] = 4 fmt.Printf("array3 => length=%d capacity=%d value=%v address=%p \n", len(array3), cap(array3), array3, &array3)
実行結果
実行すると以下となります。
array1 => length=2 capacity=2 value=[0 0] address=0xc000016050 array2 => length=4 capacity=4 value=[0 0 0 0] address=0xc00008e000 array3 => length=4 capacity=4 value=[1 2 3 0] address=0xc0000180e0 array3 => length=4 capacity=4 value=[1 2 3 4] address=0xc0000180e0
配列は要素数を宣言時に必ず決めるため、各変数での挙動が上記結果となります。アドレスを出力しているのはメモリ上に確保されるアドレスに変化が有るかどうかを見たかったためです。
配列の注意点
配列には要素を追加することは出来ないため、以下のようにするとエラーとなります。
// 配列にappendすることは出来ない array3 = append(array3, 4)
配列の値の取り出し方
パターン1
// indexは不要なので値のみを出力する for _, v := range array3 { fmt.Println(v) }
パターン2
for i := 0; i < len(array3); i++ { fmt.Println(array3[i]) }
配列まとめ
スライスについて
ここが今回のメインです。講義内でもスライスの宣言方法によってのメモリ上へどのように確保されるのかという点は触れているのですが、この時点では「まだ、気にすることはない」という事から最初はスルーしていました。深く考えると進まないのも事実です。ですが良く使いそうだなと感じたので、気になって仕方ない…という事で1度立ち止まって色々と試してみました。
宣言方法
スライスは以下のような形で宣言する事が可能です。
var slice1 []int slice2 := []int{1, 2, 3, 4} slice3 := make([]int, 10)
まずはスライスの挙動を試す
以下のようにして、スライスの長さ, 容量, 値, アドレス
を見てみます。
var slice1 []int fmt.Printf("slice1 => length=%d capacity=%d value=%v address=%p \n", len(slice1), cap(slice1), slice1, slice1) slice2 := []int{1, 2, 3, 4} fmt.Printf("slice2 => length=%d capacity=%d value=%v address=%p \n", len(slice2), cap(slice2), slice2, slice2) slice3 := make([]int, 10) fmt.Printf("slice3 => length=%d capacity=%d value=%v address=%p \n", len(slice3), cap(slice3), slice3, slice3)
実行結果
配列の時とは違い、宣言と同時に値を入れていない場合はnil
扱いとなります。make
はここでは容量を10
としていますが0
でもnil
扱いにはなりません。
また、var slice1 []int
では宣言したスライスの実体がメモリ上に確保されていますが(もしかしたら間違っているかもしれないです)どのアドレスも見ていないため0x0
になります。
slice1 => length=0 capacity=0 value=[] address=0x0 slice2 => length=4 capacity=4 value=[1 2 3 4] address=0xc000096080 slice3 => length=10 capacity=10 value=[0 0 0 0 0 0 0 0 0 0] address=0xc00009e000
makeなしのスライスを試す
make
をせず、容量を確保しない場合のスライスを宣言します。
var slice4 []int fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4) // appendするとどうなるか slice4 = append(slice4, 2) fmt.Printf("slice4 => length=%d capacity=%d value=%v address=%p \n", len(slice4), cap(slice4), slice4, slice4)
実行結果
宣言直後のslice4
では容量も確保されていないため、address
は0x0
となり、append
で値を追加すると容量が確保されています。次はmake
で宣言した場合のスライスを試していきます。
slice4 => length=0 capacity=0 value=[] address=0x0 slice4 => length=1 capacity=1 value=[2] address=0xc0000160f8
makeありのスライスを試す
ここではmake
した場合のスライスの挙動を見ていきます。確認したいポイントをPrintf
で出力していきます。やっている事としては単純です。
// makeを使う。スライスの長さは容量との関係性が見たいので0とする slice5 := make([]int, 0, 1) fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5) slice5 = append(slice5, 1) // この時点ではcapacityを超えてこないのでアドレスは変わらない fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5) slice5 = append(slice5, 2) // capacityを超えるのでアドレスが変わる fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5) slice5 = append(slice5, 3) // capacityを超えるのでまたアドレスが変わる、capacityは以前確保していたcapacity^2で増加していく fmt.Printf("slice5 => length=%d capacity=%d value=%v address=%p \n", len(slice5), cap(slice5), slice5, slice5)
実行結果
面白い結果となりました。2度目のslice5
出力までは宣言時のスライス容量を超えないためaddress
は同じです。
しかし、確保した領域を超えるとaddress
に変化があり、スライスの容量も自動で増えています。また、最後の出力時には拡張される前の容量^2
で増えています。ここではaddress
が変わっているのでslice5
の容量が拡張される度に、メモリ上に領域が確保されていきます。これは扱いを知らないと意識せずに書いてしまいそうです。
slice5 => length=0 capacity=1 value=[] address=0xc00008c160 slice5 => length=1 capacity=1 value=[1] address=0xc00008c160 slice5 => length=2 capacity=2 value=[1 2] address=0xc00008c190 slice5 => length=3 capacity=4 value=[1 2 3] address=0xc0000940e0
スライスの宣言パターンによるベンチマークを取る
スライスの宣言時に明示的に容量を確保しない場合、容量が拡張される度にメモリ上に新たな領域が確保され、無駄にメモリを喰い潰して行くという挙動だと思います。大量にループを回した場合、処理速度に大きく影響がありそうだと感じました。
Goの標準パッケージtesting
を使用してベンチマークを取り、宣言パターンによって処理速度にどれほどの差があるか見ていきます。_test.go
と終わるファイルを作成することでtest対象と出来ます。また、ベンチマークを取りたいのでBenchmark
から始まるテスト関数を作成してgo test -bench . -benchmem
で実行します。
// slice_test.go package main import "testing" // sliceの容量を指定しない場合 func BenchmarkInitSliceVariable(b *testing.B) { var target []int b.ResetTimer() for i := 0; i < b.N; i++ { target = append(target, i) } } // sliceでmakeで容量を確保しているがlength指定している場合(値が0で初期化されている場合) func BenchmarkSliceCapacityNo(b *testing.B) { var target = make([]int, b.N) b.ResetTimer() for i := 0; i < b.N; i++ { target = append(target, i) } } // sliceのmake時にc容量を設定する場合 func BenchmarkSliceCapacityYes(b *testing.B) { var target = make([]int, 0, b.N) b.ResetTimer() for i := 0; i < b.N; i++ { target = append(target, i) } }
実行結果
以下結果の通り、スライスの宣言方式で処理速度が大きく変わりました。
BenchmarkInitSliceVariable-4 100000000 22.3 ns/op 49 B/op 0 allocs/op BenchmarkSliceCapacityNo-4 200000000 93.6 ns/op 57 B/op 0 allocs/op BenchmarkSliceCapacityYes-4 2000000000 6.80 ns/op 0 B/op 0 allocs/op
実行結果についての見方は以下参考にさせていただきました。
// 関数の実行回数、有用な結果が得られるまで実行される
// 多ければ多いほど良い
2000000
// 1回の実行にかかった時間
// 少ないほど良い
815 ns/op
// 実行ごとに割り当てられたメモリのサイズ
// 少ないほど良い
336 B/op
// 1回の実行でメモリアロケーションが行われた回数
// 少ないほど良い
9 allocs/op
今回の結果に当てはめると以下ですね。
// 関数の実行回数、有用な結果が得られるまで実行される(多いほど良い) 100000000 200000000 2000000000 // 1回の実行にかかった時間(少ないほど良い) 22.3 ns/op 93.6 ns/op 6.80 ns/op // 実行ごとに割り当てられたメモリのサイズ(少ないほど良い) 49 B/op 57 B/op 0 B/op //1回の実行でメモリアロケーションが行われた回数(少ないほど良い) 0 allocs/op 0 allocs/op 0 allocs/op
スライスまとめ
- 要素数は可変長
- スライスの
make
時は要素数がわかっているなら宣言時にlen
で確保 make([]int, 要素数)
としてしまいそう注意が必要- 宣言時は長さが膨大だとスライスの中身は型による初期化が行われる ので
length
は0
が良い
まとめ
講義の内容では約10分ほどの配列とスライスですが、とても考えさせられました。アロケーションが0
だったのが私の考えていた想定と違ったので、アロケーション回数が大きく増えてしまうようなバッドプラクティスなども今後学習を続ける中で深堀りしていきたいと思っています。