はじめに
前回は、スライスマスターになりたいので、Slice Tricks(チートシート)を作って見ました💪
しかし、図が大きかったり、長く読みにくい記事を書いてしまったので、
前回記事にした記事の重要なところだけ(記事を書いていて学びの多かったところ)を抜粋しました!🙆
特に、忙しい人向けに、自分が重要だと感じるところを共有する記事(チートシート)です!🙇
元ネタ
🏆 スライスの要素追加, 結合
-
for _, i := range b { a = append(a, i) }
-
a = append(a, b...)
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
for _, i := range s2 {
s1 = append(s1, i)
}
fmt.Println(s1) // [1 2 3 4 5 6]
s3 := []int{1, 2, 3}
s4 := []int{4, 5, 6}
s3 = append(s3, s4...)
fmt.Println(s3) // [1 2 3 4 5 6]
💡 cap(容量)を超えてしまったら?
スライス b の全要素をスライス a の末尾に追加するとき、aのcap(容量)を超えてしまったらどうなるか
→ 元のスライスの2倍のcap(容量)を持つ新しいスライスが作成され、そこにaのコピーと、末尾にbが追加される
このため、元のスライスの要素数が非常に大きい場合や、追加する要素数が多い場合は、必要となるメモリ量とデータのコピーにかかる時間が増加し、パフォーマンスに影響が出ます💦
対策 : 事前に必要なスライスのサイズが分かっている場合、または大量のデータを扱う場合は、make 関数を使って十分なcap(容量)を持つスライスを事前に確保しておくことが大切です!
💡 コピーするか直接読み出すか
大きいなスライスを扱う場合は、for i := range slice
を使う!
for i, v := range map[] or slice
を使った場合
i
, v
にはコピーの操作が走るため、大きなデータになるほど遅くなってしまう💦
s := []int{1, 2, 3, 4, 5}
// インデックス(i)とその値のコピー(v)を生成
for i, v := range s {
fmt.Println(i)
fmt.Println(v)
}
// スライスから直接値を読み出す
for i := range s {
fmt.Println(s[i])
}
対策 : 大きいスライスを扱う場合は、for i := range slice
を使う! i
を使って、slice[i]
で要素にアクセス
🏆 スライスのコピー
-
b = make([]T, len(a))
copy(b, a)
- copy()関数を用いてaの全要素をbにコピー
-
b = append([]T(nil), a...)
- aの要素は新しいスライスにコピーされ、そのコピーされたスライスがbに代入される
-
b = append(a[:0:0], a...)
- a[:0:0]は、スライスaから新たなスライス(aと同じcap(容量)を持つがlen(長さ)が0の新たなスライス)を作成
- そして、そのスライスにaの全要素を追加
s1 := []int{1, 2, 3, 4, 5}
b1 := make([]int, len(s1))
n := copy(b1, s1)
fmt.Println(n, b1) // 5 [1 2 3 4 5]
s2 := []int{1, 2, 3, 4, 5}
b2 := append([]int(nil), s2...)
fmt.Println(b2) // 出力: [1 2 3 4 5]
s3 := []int{1, 2, 3, 4, 5}
b3 := append(s3[:0:0], s3...)
fmt.Println(b3) // 出力: [1 2 3 4 5]
💡 append
とcopy
について
append
関数
- スライスの末尾に新しい要素を追加する。
- スライスの容量が十分であれば、新しいメモリ領域を確保することなく操作が行われる。
cap(容量)が足りなくなると(追加する要素がスライスの容量を超える場合)
- 新たなメモリ領域を確保し、元のスライスを確保したメモリ領域にコピーをします。
- この新たなメモリ領域の確保は、既存のスライス容量の2倍の大きさを確保します。
copy
関数
- 新たなメモリ領域を確保をせずに、元のスライスを新たなスライスにコピーします。
→つまり、cap(容量)が足りなくなる要素のコピーでは、append
を使用すると、新たなメモリ領域を確保(メモリアロケーション)するので、copy
関数を用いてスライスの操作を行う方がいいと感じます!
結論 : append
とcopy
は、パフォーマンス検証しながら、適切な方法を選択することが重要ですね🙆♂️
-
大きなスライスを扱う時(cap(容量)を超えた操作)
copy
関数でスライスの操作を行う方がいいかも
理由:append
だと不必要なメモリの確保と解放が多発(ガベージコレクション(GC)が頻繁に発生)し、パフォーマンスが低下する。 -
小さいスライスを扱う時(cap(容量)内の操作)
append
関数でスライスの操作を行う方がいいかも
理由:コードの読みやすさやメンテナンス性が上がるかも(append関数のほうが他コードや記事でよく見る👀)
参考
append
とcopy
について詳しく知りたい方へ
🏆 スライスをソートする
-
sort.Slice(a, func(i, j T) bool { return a[i] < a[j] })
- 不安定ソート: 並び替えを行う際に、同じ値を持つ要素があっても、元の順序を気にせず相対的な順序になる
-
sort.SliceStable(a, func(i, j T) bool { return a[i] < a[j] })
- 安定ソート: 並び替えを行う際に、同じ値を持つ要素があったら、元の順序を保持する
-
sort.Ints(a)
- 不安定ソート: sort.IntSlice(a) を作成し、その上で sort.Sort を実行している。
-
sort.Stable(sort.IntSlice(a))
- 安定ソート: sort.IntSlice(a) を作成し、その上で sort.Stable を実行している。
// スライスを不安定ソート
s1 := []int{3, 4, 5, 1, 2}
sort.Slice(s1, func(i, j int) bool {
return s1[i] < s1[j]
})
fmt.Println(s1) // [1 2 3 4 5]
// スライスを安定ソートする
s2 := []int{3, 4, 5, 1, 2}
sort.SliceStable(s2, func(i, j int) bool {
return s2[i] < s2[j]
})
fmt.Println(s2) // [1 2 3 4 5]
// スライスを不安定ソート
s3 := []int{3, 4, 5, 1, 2}
sort.Ints(s3)
fmt.Println(s3) // [1 2 3 4 5]
// スライスを安定ソートする
s4 := []int{3, 4, 5, 1, 2}
sort.Stable(sort.IntSlice(s4))
fmt.Println(s4) // [1 2 3 4 5]
💡 安定ソートと不安定ソートの違い
スライスを使って安定ソートと不安定ソートの違いを視覚的に理解するのは難しいので、マップを使って説明します。。。🙏
// スライスを不安定ソート
p := []P{{"A", 20}, {"B", 20}, {"C", 25}, {"D", 20}}
sort.Slice(p, func(i, j int) bool { return p[i].a < p[j].a })
for _, v := range p {
fmt.Printf("%s: %d\n", v.n, v.a)
}
// "A", "B", "D"(abcd順)の順序はソートごとに変わる可能性がある
// A: 20
// D: 20
// B: 20
// C: 25
// スライスを安定ソートする
p = []P{{"A", 20}, {"B", 20}, {"C", 25}, {"D", 20}}
sort.SliceStable(p, func(i, j int) bool { return p[i].a < p[j].a })
for _, v := range p {
fmt.Printf("%s: %d\n", v.n, v.a)
}
// "A", "B", "D"(abcd順)の順序はソート前と同じ
// A: 20
// B: 20
// D: 20
// C: 25
🏆 メモリリークを防ぐ
メモリリーク
プログラムが動作する上で確保したメモリを適切に解放せず、不要になったメモリ領域がガベージコレクション(不要になったメモリの自動回収)の対象とならない状況です。これにより、プログラムは実際には必要としていないにもかかわらず、次第に増え続けるメモリ領域を占有し続けることになる(大変だ💦)
この結果、システム全体のパフォーマンスが低下したり、最悪の場合、メモリが枯渇してプログラムがクラッシュしたりする可能性がある。
こんな場面の時(メモリリークの例)
// 大きなスライスを作成
largeSlice := make([]int, 1<<20) // 1MBのスライス
// スライスからサブスライスを作成
subslice := largeSlice[10:20]
// これ以降、subsliceだけを使って処理を行いたい...
何がダメなのか、、、
subsliceはlargeSliceの一部を参照しています。
しかし、その後でlargeSliceが不要になったとしても、subsliceがまだlargeSliceを参照しているため、largeSliceはガベージコレクションの対象にならず、そのメモリは解放されない!!!
対策: 不必要になったデータへの参照を持ち続けないようにする。つまり、新たなスライスにデータをcopy
する
// 大きなスライスを作成
largeSlice := make([]int, 1<<20) // 1MBのスライス
// 新たなスライスにデータをコピー
subslice := make([]int, 10)
copy(subslice, largeSlice[10:20])
// これ以降、subsliceだけを使って処理を行いたい...
subsliceは新たに確保されたメモリ領域にlargeSliceからデータをコピーして作成されています。
そのため、largeSliceが不要になった時点でそのメモリは解放され、メモリリークは発生しない。
💡 sync.Pool
sync.Pool
は、頻繁に大きなスライスを作成し、短期間でそのスライスが不要になる場合、オブジェクトの一時的な再利用を可能する!
スライスをプール化することで、メモリアロケーションのコストを削減したいときや、ガベージコレクションの負荷を減らすことができてすごい!😏
// Poolを作成します。ここで、新しいオブジェクトを生成するための関数を定義
// この例では、1024の長さを持つint型のスライスを生成
var pool = &sync.Pool{
New: func() interface{} {
return make([]int, 1024)
},
}
func main() {
for i := 0; i < 10; i++ {
// Get() Poolからスライスを取得
s := pool.Get().([]int)
// 何らかのデータ処理
fmt.Println(i)
for j := 0; j < 1024; j++ {
s[j] = i
}
// Put() 使用後はスライスを再利用するためにPoolに戻す
pool.Put(s)
}
}
参考
ガベージコレクション(GC)が知らない方へ(見慣れない言語ですが、すごくわかりやすい)
sync.Pool
について
おわりに
フルの記事はこちらにあります!