0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】スライスの挙動,スライスの要素削除について調べた

Posted at

最近Goを使い始めたのですが、スライスの要素削除(フィルタリング)について調べていたところ、アロケーション(メモリ確保)を発生させずに高速に処理する書き方を知りました。

なぜその書き方で動くのか、スライスの挙動を含めて調べてみたので備忘録としてまとめます。

やりたかったこと

既存のスライスから、特定の条件の要素だけを残して他を削除したい。
単純に make で新しいスライスを作るとメモリの確保が起きてしまうので、既存の領域をうまく使い回したい。

結論:こう書く

以下のように書くことで、メモリ割り当てを行わずにフィルタリングできました。

func filterMembers(members []int, removeID int) []int {
    // 1. 元の配列を共有する長さ0のスライスを作る
    next := members[:0] 

    for _, id := range members {
        if id != removeID {
            // 2. appendで条件に合うものだけ詰める
            // (実体の配列が上書きされる)
            next = append(next, id)
        }
    }

    // 3. 結果を返す
    return next
}

ポイントは next := members[:0] です。
一見不思議な書き方ですが、これがどう動いているのかを整理します。

仕組みの理解

スライスと「実体の配列」

Goのスライスについて調べてみると、スライス自体はデータそのものを持っているわけではないことが分かりました。
スライスは、メモリ上のどこかにある 「実体の配列」 を参照しているだけのようです。

具体的には、スライス変数は以下の3つの情報を持っています。

  1. ptr: 実体の配列のどこから見るか(開始位置)
  2. len: 実体の配列をいくつ見るか(長さ)
  3. cap: 実体の配列の容量限界

イメージとしては以下のような感じです。

[ スライス変数 members ]      [ 実体の配列 ]
+-----+-----+-----+          +----+----+----+----+
| ptr | len | cap | -------> | 10 | 20 | 30 | 40 |
+-----+-----+-----+          +----+----+----+----+
   |     4     4               ^
   |                           |
   +---------------------------+

members[:0] で起きること

ここで next := members[:0] を実行すると、「同じ実体の配列を指しつつ、lenだけを0にした新しいスライス」 が作られます。

[ スライス変数 next ]
+-----+-----+-----+
| ptr | len | cap | --+  (同じ実体を共有!)
+-----+-----+-----+   |
   |     0     4      |
   +------------------+
                      |
                      v
[ スライス変数 members ]      [ 実体の配列 ]
+-----+-----+-----+          +----+----+----+----+
| ptr | len | cap | -------> | 10 | 20 | 30 | 40 |
+-----+-----+-----+          +----+----+----+----+
         4     4

重要なのは、新しい実体を作るのではなく、既存の実体を使い回している という点です。
そのため、メモリのアロケーションが発生しません。

append での上書き

この next(長さ0)に対して append を行うと、共有している 実体の配列 の先頭から値が上書きされていきます。

例えば 20 を削除(スキップ)して 10, 30, 40 を残す場合、ループが進むにつれて実体の中身は以下のように変化します。

  1. 10 を append → 実体の [0]10 を上書き(変化なし)
  2. 20 はスキップ
  3. 30 を append → 実体の [1]30 を上書き
  4. 40 を append → 実体の [2]40 を上書き

ループ終了後の状態:

実体の配列: [ 10, 30, 40, 40 ]
              ^   ^   ^   ^
              |   |   |   └- (ゴミとして残る古い値)
              |   |   └----- nextの3つ目として上書き
              |   └--------- nextの2つ目として上書き
              └------------- nextの1つ目として上書き

最後に変数の更新が必要

上記の図の通り、実体の中身は書き換わりましたが、元の変数 members はまだ len=4(4つ全部見る設定)のままです。
このままだと、末尾に残った古い 40 まで見えてしまいます。

そのため、最後に return next で新しいスライスを返すか、members = next と代入してスライスの情報を更新する必要があるわけです。

まとめ

  • s[:0] は、実体の配列 を共有したまま長さを0にするテクニック。
  • これを利用すると、append 時に新しいメモリ確保を行わず、実体の中身を直接上書きできる。
  • スライスはあくまで「実体を見るための窓口」であり、実体そのものではない。

この挙動を理解しておくと、パフォーマンスを意識したコードが書けそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?