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 full-size slide

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

    View full-size slide

  3. 今回のテーマ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. 容量の拡張量は?

    View full-size slide

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


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


    View full-size slide

  8. 確かめてみよう

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. 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 full-size slide

  13. ~略~
    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 full-size slide

  14. 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 full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. roundupsize?

    View full-size slide

  18. 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 full-size slide

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

    View full-size slide

  20. 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 full-size slide

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


    View full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  26. 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 full-size slide

  27. 拡張前容量 * 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 full-size slide

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

    View full-size slide