爲何遍歷 Go map 是無序的?

image

有的小夥伴沒留意過 Go map 輸出順序,覺得它是穩定的有序的;有的小夥伴知道是無序的,但殊不知道爲何?有的卻理解錯誤?今天咱們將經過本文,揭開 for range map 的 「神祕」 面紗,看看它內部實現究竟是怎麼樣的,輸出順序究竟是怎麼樣?html

原文地址:爲何遍歷 Go map 是無序的?golang

前言

func main() {
    m := make(map[int32]string)
    m[0] = "EDDYCJY1"
    m[1] = "EDDYCJY2"
    m[2] = "EDDYCJY3"
    m[3] = "EDDYCJY4"
    m[4] = "EDDYCJY5"

    for k, v := range m {
        log.Printf("k: %v, v: %v", k, v)
    }
}

假設運行這段代碼,輸出結果是按順序?仍是無序輸出呢?面試

2019/04/03 23:27:29 k: 3, v: EDDYCJY4
2019/04/03 23:27:29 k: 4, v: EDDYCJY5
2019/04/03 23:27:29 k: 0, v: EDDYCJY1
2019/04/03 23:27:29 k: 1, v: EDDYCJY2
2019/04/03 23:27:29 k: 2, v: EDDYCJY3

從輸出結果上來說,是非固定順序輸出的,也就是每次都不同(標題也講了)。但這是爲何呢?api

首先建議你先本身想一想緣由。其次我在面試時聽過一些說法。有人說由於是哈希的因此就是無(亂)序等等說法。當時我是有點 ???ide

這也是這篇文章出現的緣由,但願你們能夠一塊兒研討一下,理清這個問題 :)函數

看一下彙編

...
    0x009b 00155 (main.go:11)    LEAQ    type.map[int32]string(SB), AX
    0x00a2 00162 (main.go:11)    PCDATA    $2, $0
    0x00a2 00162 (main.go:11)    MOVQ    AX, (SP)
    0x00a6 00166 (main.go:11)    PCDATA    $2, $2
    0x00a6 00166 (main.go:11)    LEAQ    ""..autotmp_3+24(SP), AX
    0x00ab 00171 (main.go:11)    PCDATA    $2, $0
    0x00ab 00171 (main.go:11)    MOVQ    AX, 8(SP)
    0x00b0 00176 (main.go:11)    PCDATA    $2, $2
    0x00b0 00176 (main.go:11)    LEAQ    ""..autotmp_2+72(SP), AX
    0x00b5 00181 (main.go:11)    PCDATA    $2, $0
    0x00b5 00181 (main.go:11)    MOVQ    AX, 16(SP)
    0x00ba 00186 (main.go:11)    CALL    runtime.mapiterinit(SB)
    0x00bf 00191 (main.go:11)    JMP    207
    0x00c1 00193 (main.go:11)    PCDATA    $2, $2
    0x00c1 00193 (main.go:11)    LEAQ    ""..autotmp_2+72(SP), AX
    0x00c6 00198 (main.go:11)    PCDATA    $2, $0
    0x00c6 00198 (main.go:11)    MOVQ    AX, (SP)
    0x00ca 00202 (main.go:11)    CALL    runtime.mapiternext(SB)
    0x00cf 00207 (main.go:11)    CMPQ    ""..autotmp_2+72(SP), $0
    0x00d5 00213 (main.go:11)    JNE    193
    ...

咱們大體看一下總體過程,重點處理 Go map 循環迭代的是兩個 runtime 方法,以下:flex

  • runtime.mapiterinit
  • runtime.mapiternext

但你可能會想,明明用的是 for range 進行循環迭代,怎麼出現了這兩個函數,怎麼回事?ui

看一下轉換後

var hiter map_iteration_struct
for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
    index_temp = *hiter.key
    value_temp = *hiter.val
    index = index_temp
    value = value_temp
    original body
}

實際上編譯器對於 slice 和 map 的循環迭代有不一樣的實現方式,並非 for 一扔就完事了,還作了一些附加動做進行處理。而上述代碼就是 for range map 在編譯器展開後的僞實現spa

看一下源碼

runtime.mapiterinit

func mapiterinit(t *maptype, h *hmap, it *hiter) {
    ...
    it.t = t
    it.h = h
    it.B = h.B
    it.buckets = h.buckets
    if t.bucket.kind&kindNoPointers != 0 {
        h.createOverflow()
        it.overflow = h.extra.overflow
        it.oldoverflow = h.extra.oldoverflow
    }

    r := uintptr(fastrand())
    if h.B > 31-bucketCntBits {
        r += uintptr(fastrand()) << 31
    }
    it.startBucket = r & bucketMask(h.B)
    it.offset = uint8(r >> h.B & (bucketCnt - 1))
    it.bucket = it.startBucket
    ...

    mapiternext(it)
}

經過對 mapiterinit 方法閱讀,可得知其主要用途是在 map 進行遍歷迭代時進行初始化動做。共有三個形參,用於讀取當前哈希表的類型信息、當前哈希表的存儲信息和當前遍歷迭代的數據code

爲何

我們關注到源碼中 fastrand 的部分,這個方法名,是否是迷之眼熟。沒錯,它是一個生成隨機數的方法。再看看上下文:

...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
    r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))

// iterator state
it.bucket = it.startBucket

在這段代碼中,它生成了隨機數。用於決定從哪裏開始循環迭代。更具體的話就是根據隨機數,選擇一個桶位置做爲起始點進行遍歷迭代

所以每次從新 for range map,你見到的結果都是不同的。那是由於它的起始位置根本就不固定!

runtime.mapiternext

func mapiternext(it *hiter) {
    ...
    for ; i < bucketCnt; i++ {
        ...
        k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
        v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.valuesize))
        ...
        if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
            !(t.reflexivekey || alg.equal(k, k)) {
            ...
            it.key = k
            it.value = v
        } else {
            rk, rv := mapaccessK(t, h, k)
            if rk == nil {
                continue // key has been deleted
            }
            it.key = rk
            it.value = rv
        }
        it.bucket = bucket
        if it.bptr != b { 
            it.bptr = b
        }
        it.i = i + 1
        it.checkBucket = checkBucket
        return
    }
    b = b.overflow(t)
    i = 0
    goto next
}

在上小節中,我們已經選定了起始桶的位置。接下來就是經過 mapiternext 進行具體的循環遍歷動做。該方法主要涉及以下:

  • 從已選定的桶中開始進行遍歷,尋找桶中的下一個元素進行處理
  • 若是桶已經遍歷完,則對溢出桶 overflow buckets 進行遍歷處理

經過對本方法的閱讀,可得知其對 buckets 的遍歷規則以及對於擴容的一些處理(這不是本文重點。所以沒有具體展開)

總結

在本文開始,我們先提出核心討論點:「爲何 Go map 遍歷輸出是不固定順序?」。而經過這一番分析,緣由也很簡單明瞭。就是 for range map 在開始處理循環邏輯的時候,就作了隨機播種...

你想問爲何要這麼作?固然是官方有意爲之,由於 Go 在早期(1.0)的時候,雖是穩定迭代的,但從結果來說,實際上是沒法保證每一個 Go 版本迭代遍歷規則都是同樣的。而這將會致使可移植性問題。所以,改之。也請不要依賴...

參考

相關文章
相關標籤/搜索