golang で わりとみな(まぁわかってる人はわかってるけど) なんとなく使っているけどわりとみんな疑問におもっていることにslice がある。

けっこう軽くぐぐるとslice について書いてるものはわりといろいろある。

ぶっちゃけ仕事で関わった中で(仕事以外の人はだいたいわかってるw)slice をそれなりに理解して使ってる人を一度もみたことない。 そもそもこ上にあるlink を見てるだけで意識が高いわけだ。

だいたい

そのコードでの使われるであろうsliceの長さがわかるのに、毎回 cap=0, len=0で関数で作ってひたすらappend し続ける

全部使われるとは限らないのに 固定のlen == cap で作成して、でそのあとappend する

こんなのだ。こういうコードがあるとだいたい理解してないのを察知する

吾輩は go のslice は非常に好きだ。それなりにメモリのアロケーションのコントロールができる 上で簡便にできている。基本は c++ のvector と似たようなものではあるができることは大差ないが 簡便さが違うと感じている

メモリを気にする事案が多いのでc++ を使う場合もgolang を使う場合もやはりそのあたりによく触れるから(仕事でやってる場合で他の人がなんもわかってないのが多いから)というのもある。

slice のatomicity と concurrency に関して

それでもいくつかの不満は微妙にある。それはatomicity と concurrency に関してだ。 golang は基本方針として 異なるthread とかgoroutine でデータは共有するな という方針ではある。なので当然 slice はconcurrent ではない。例外として完全リードオンリー なものであれば問題ない。(そもそもそれ成り立たないと CSP も成立しない)

go のslice がどういうものかというのは世の中に上の記事を筆頭にいっぱいころがってるので詳しくは触れないが

かんたんにいうと

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

このstruct だ。(runtime/slice.goから) これにアクセスするには通常 reflect.SliceHeader があって

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

ぶっちゃけslice をあまり理解してない人というかslice の説明するのに最初にこれを提示したほうがいいとおもうなので,入門用のマニュアルのたぐいはそれがまず、あまり良くないのではないかと個人的に思う。

a := []Hoge{}

みたいなのがあったとして 実際はこのSliceHeader のstruct が値渡しされてるだけだったりする。

append() で現在のcap より多い値を追加するような場合は

slice.array がrealloc されるのか malloc されるのかわからんが新たなな別のポインタとして 割り当てられる(まぁstack の場合もありうるけどそれにはあまり触れない)

skiplistmap をいじっていたときもそうなのだけど、

a := make([]int, 0, 10)

a = a[:1]

capcity 内で長さだけ増やしたい場合がある。 しかしこのオペレーションはatomicではない。

そもそも struct なのでcopy した側でいじれという方針だと思われる。しかし struct のメンバーのslice の長さを変えるときは困ってしまう。

そういった場合は,sliceheader つかえよってことなのですかね。 個人的 slice のlen の変更は atomic にしてほしい

type mySLiceHeader struct {
	data unsafe.Pointer
	len   int
	cap   int
}

a := make([]int, 0, 10)

shead := (*mySLiceHeader)(atomic.LoadPointer(
    (*unsafe.Pointer)(unsafe.Pointer(&a))))

atomic.CompareAndSwapInt(&shead.len, &shead.len, shead.len+1)

こうすればatomic だ。capの増加は mySLiceHeader.data が変わるのでいろいろアレだが、 len と cap の減少ぐらいはサポートしてほしいなぁ。と個人的には思います。

個人的な理想としては

  • len の増加はatomic にしてちょ
  • cap の減少もatomic にしてちょ

そしたら吾輩がsliceheader 使う機会は減る。w

cap 増加時の atomic

cap を増やすときはまぁだいたい想像できるとおもうが

atomic.CompareAndSwapPointer(&shead.data, shead.data, new)
atomic.CompareAndSwapInt(&shead.cap, shead.cap, shead.cap +1)

こうすればよかったりするが流石にこれはappend に任せたほうがいいと思うので どうしてもって場合以外はしないほうがいいと思われる

そういえばなんで reflect.SliceHeader は

  • reflect にはheader のオペレーションがほとんどないのに定義されてるのだろう? Slice系のreflect の関数でつかってる?
  • SliceHeader.Data がなんで uintptr なんだろう? unsafe.Pointer ではなくして boundary check から外したいってことなんですかね? それともuintptr にしないと動かない事情があるのだろうか?