Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goのスライス容量拡張量がどのように決まるのか追った / 180713 LT

Goのスライス容量拡張量がどのように決まるのか追った / 180713 LT

kaznishi

July 13, 2018
Tweet

More Decks by kaznishi

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. 今回のテーマ

    View Slide

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

    View Slide

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

    View Slide

  6. 容量の拡張量は?

    View Slide

  7. 容量の拡張量は?
    「プログラミング言語Go」より
    「Goならわかるシステムプログラミング」より
    拡張ごとに配列の大きさを倍にすることにより過
    剰な回数の割り当てを避け、一つの要素の追加が
    平均的に定数時間で済むことを保証しています。


    もし、余裕がない状態でappend()を呼ぶと、cap()
    の2倍のメモリを確保し、今までの要素をコピー
    したうえで新しい要素を新しいメモリ領域に追加
    します。


    View Slide

  8. 確かめてみよう

    View Slide

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

    View Slide

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

    View Slide

  11. はて

    View Slide

  12. Goの実装を追ってみた

    View Slide

  13. 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
    }
    ~略~

    View Slide

  14. ~略~
    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)

    View Slide

  15. 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)
    }

    View Slide

  16. newcapに調整がかかってる

    View Slide

  17. 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 要素サイズ)
    補正後スライス容量 = 確保メモリ / 要素サイズ

    View Slide

  18. roundupsize?

    View Slide

  19. 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)
    }

    View Slide

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

    View Slide

  21. 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,..
    これは一体…?

    View Slide

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


    View Slide

  23. 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.


    View Slide

  24. 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." とのことです。)

    View Slide

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

    View Slide

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

    View Slide

  27. 再掲

    View Slide

  28. 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)

    View Slide

  29. 拡張前容量 * 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

    View Slide

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

    View Slide

  31. おわり

    View Slide