Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
Goのスライス容量拡張量がどのように決まるのか追った / 180713 LT
kaznishi
July 13, 2018
Programming
3
2.7k
Goのスライス容量拡張量がどのように決まるのか追った / 180713 LT
kaznishi
July 13, 2018
Tweet
Share
More Decks by kaznishi
See All by kaznishi
kaznishi
2
2.7k
kaznishi
0
210
kaznishi
4
2k
kaznishi
0
50
Other Decks in Programming
See All in Programming
kazuhei0108
3
960
rukiadia
3
890
bosshawk
1
270
drumato
1
250
nerocrux
3
1.7k
coe401_
3
170
samjulien
0
400
hkusu
0
280
toedter
0
130
kulkarniankita09
0
270
timeseriesfr
0
120
y__mattu
0
190
Featured
See All Featured
dotmariusz
94
5.5k
sachag
267
17k
qrush
285
19k
imathis
479
150k
afnizarnur
176
14k
searls
204
37k
trallard
15
820
lynnandtonic
272
16k
frogandcode
128
20k
reverentgeek
27
2.1k
danielanewman
2
560
marcelosomers
221
15k
Transcript
Goのスライス容量拡張量が どのように決まるのか追った 2018-07-13 golang.tokyo #16 LT by kaznishi
自己紹介 Twitter: @kaznishi1246 主な守備範囲: サーバーサイド,インフラ 主な使用言語: PHP, Scala Go歴: 約1ヶ月
(≒Gopher道場#2)
今回のテーマ
スライスが満容量のときに appendで追加される容量の話
復習 スライスは配列の部分列への参照のためのデータ 構造 配列は固定長 容量が足りなくなった場合、容量が拡張された新 たな配列が作られ、参照先が切り替わる
容量の拡張量は?
容量の拡張量は? 「プログラミング言語Go」より 「Goならわかるシステムプログラミング」より 拡張ごとに配列の大きさを倍にすることにより過 剰な回数の割り当てを避け、一つの要素の追加が 平均的に定数時間で済むことを保証しています。 “ “ もし、余裕がない状態でappend()を呼ぶと、cap() の2倍のメモリを確保し、今までの要素をコピー
したうえで新しい要素を新しいメモリ領域に追加 します。 “ “
確かめてみよう
Go Playgroundで確認 cap = 4 のとき https://play.golang.org/p/zPLWMUM2gzw OK
Go Playgroundで確認 cap = 5 のとき https://play.golang.org/p/eyPOVocDUc- 「12」!!!???
はて
Goの実装を追ってみた
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 } ~略~
~略~ 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)
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) }
newcapに調整がかかってる
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 要素サイズ) 補正後スライス容量 = 確保メモリ / 要素サイズ
roundupsize?
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) }
roundupsizeというからにはメモリの確保量を切り 上げているのだろうが、何のための切り上げ? class_to_size , size_to_class の'class'とは?
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,.. これは一体…?
「Goならわかるシステムプログラミング」より 小さなオブジェクトについては、より小さな単位 の「クラス」という分類で空きメモリのリストを 持っています。クラスからのメモリ取得では、リ クエストされたサイズに近いクラスの空きリスト があればそこからメモリを確保します。この場合 にはロックが不要であり、それだけ高速に処理で きます。 “ “
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. “ “
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." とのことです。)
roundupsizeにおけるclassとは サイズをレンジごとに分類するもの 分類されたクラスごとに空きメモリリストを持つ 空きメモリリストからのメモリ確保は高速な処理 が可能
意味が掴めたところでnewcapの計算 結果を確かめてみる
再掲
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)
拡張前容量 * 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
まとめ スライスの容量拡張される際の新容量は、元容量 の大体2倍である。 (今回の話から省いたが、スラ イス長が大きい場合(1024が閾値)は大体1.25倍) きっちり2倍にならないのは、TCMallocの処理が メモリ確保を高速化するため、クラスで定められ たサイズまでメモリ確保量が切り上げされている からである。
おわり