Slide 1

Slide 1 text

Goのスライス容量拡張量が どのように決まるのか追った 2018-07-13 golang.tokyo #16 LT by kaznishi

Slide 2

Slide 2 text

自己紹介 Twitter: @kaznishi1246 主な守備範囲: サーバーサイド,インフラ 主な使用言語: PHP, Scala Go歴: 約1ヶ月 (≒Gopher道場#2)

Slide 3

Slide 3 text

今回のテーマ

Slide 4

Slide 4 text

スライスが満容量のときに appendで追加される容量の話

Slide 5

Slide 5 text

復習 スライスは配列の部分列への参照のためのデータ 構造 配列は固定長 容量が足りなくなった場合、容量が拡張された新 たな配列が作られ、参照先が切り替わる

Slide 6

Slide 6 text

容量の拡張量は?

Slide 7

Slide 7 text

容量の拡張量は? 「プログラミング言語Go」より 「Goならわかるシステムプログラミング」より 拡張ごとに配列の大きさを倍にすることにより過 剰な回数の割り当てを避け、一つの要素の追加が 平均的に定数時間で済むことを保証しています。 “ “ もし、余裕がない状態でappend()を呼ぶと、cap() の2倍のメモリを確保し、今までの要素をコピー したうえで新しい要素を新しいメモリ領域に追加 します。 “ “

Slide 8

Slide 8 text

確かめてみよう

Slide 9

Slide 9 text

Go Playgroundで確認 cap = 4 のとき https://play.golang.org/p/zPLWMUM2gzw OK

Slide 10

Slide 10 text

Go Playgroundで確認 cap = 5 のとき https://play.golang.org/p/eyPOVocDUc- 「12」!!!???

Slide 11

Slide 11 text

はて

Slide 12

Slide 12 text

Goの実装を追ってみた

Slide 13

Slide 13 text

https://github.com/golang/go/blob/master/src/runti me/slice.go func growslice(et *_type, old slice, cap int) slice { ~略~ newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } ~略~

Slide 14

Slide 14 text

~略~ switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize)

Slide 15

Slide 15 text

case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem = roundupsize(uintptr(newcap) * et.size) overflow = uintptr(newcap) > maxSliceCap(et.size) newcap = int(capmem / et.size) }

Slide 16

Slide 16 text

newcapに調整がかかってる

Slide 17

Slide 17 text

switch { case et.size == sys.PtrSize: ~略~ capmem = roundupsize(uintptr(newcap) * sys.PtrSize) ~略~ newcap = int(capmem / sys.PtrSize) default: ~略~ capmem = roundupsize(uintptr(newcap) * et.size) ~略~ newcap = int(capmem / et.size) } 確保メモリ = roundupsize(補正前スライス容量 x 要素サイズ) 補正後スライス容量 = 確保メモリ / 要素サイズ

Slide 18

Slide 18 text

roundupsize?

Slide 19

Slide 19 text

https://github.com/golang/go/blob/master/src/runti me/msize.go func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8 [(size+smallSizeDiv-1)/smallSizeDiv]]) } else { return uintptr(class_to_size[size_to_class128 [(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]) } } if size+_PageSize < size { return size } return round(size, _PageSize) }

Slide 20

Slide 20 text

roundupsizeというからにはメモリの確保量を切り 上げているのだろうが、何のための切り上げ? class_to_size , size_to_class の'class'とは?

Slide 21

Slide 21 text

https://github.com/golang/go/blob/master/src/runti me/sizeclasses.go // class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // ... ... .... ... 〜略〜 var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, ... 〜略〜 var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4,.. これは一体…?

Slide 22

Slide 22 text

「Goならわかるシステムプログラミング」より 小さなオブジェクトについては、より小さな単位 の「クラス」という分類で空きメモリのリストを 持っています。クラスからのメモリ取得では、リ クエストされたサイズに近いクラスの空きリスト があればそこからメモリを確保します。この場合 にはロックが不要であり、それだけ高速に処理で きます。 “ “

Slide 23

Slide 23 text

https://github.com/golang/go/blob/master/src/runti me/malloc.go のコメントより 1. Round the size up to one of the small size classes and look in the corresponding mspan in this P's mcache. Scan the mspan's free bitmap to nd a free slot. If there is a free slot, allocate it. This can all be done without acquiring a lock. “ “

Slide 24

Slide 24 text

TCMalloc実装の一部分 TCMallocについては下記URLに詳細 http://goog- perftools.sourceforge.net/doc/tcmalloc.html ただしGoにおけるTCMallocはGo向けにアレンジ されたもの。(malloc.goのコメントによると "This was originally based on tcmalloc, but has diverged quite a bit." とのことです。)

Slide 25

Slide 25 text

roundupsizeにおけるclassとは サイズをレンジごとに分類するもの 分類されたクラスごとに空きメモリリストを持つ 空きメモリリストからのメモリ確保は高速な処理 が可能

Slide 26

Slide 26 text

意味が掴めたところでnewcapの計算 結果を確かめてみる

Slide 27

Slide 27 text

再掲

Slide 28

Slide 28 text

old.cap = 5 Go Playground環境(GOOS=NaCl, GOARCH=amd64p32)においてはintのet.sizeは4, sys.PtrSizeも4 newcap := old.cap doublecap := newcap + newcap ~略~ if old.len < 1024 { newcap = doublecap } else { ~略~ case et.size == sys.PtrSize: ~略~ capmem = roundupsize(uintptr(newcap) * sys.PtrSize) ~略~ newcap = int(capmem / sys.PtrSize)

Slide 29

Slide 29 text

拡張前容量 * 2 = 5 * 2 = 10 ↓ roundupsize前のcapmem = 10 * 4 = 40(bytes) ↓ class 4 (33~48 bytes) に分類 ↓ roundupsizeで 48 bytes に切り上げ ↓ 新しいスライス容量 = 48 / 4 = 12 計算結果が12であることを確認! https://play.golang.org/p/oSn8GbSWVkK

Slide 30

Slide 30 text

まとめ スライスの容量拡張される際の新容量は、元容量 の大体2倍である。 (今回の話から省いたが、スラ イス長が大きい場合(1024が閾値)は大体1.25倍) きっちり2倍にならないのは、TCMallocの処理が メモリ確保を高速化するため、クラスで定められ たサイズまでメモリ確保量が切り上げされている からである。

Slide 31

Slide 31 text

おわり