递增地图的最快方法是什么?

时间:2018-10-04 12:17:03

标签: go optimization hashmap

我注意到map[int]int变量的以下两个增量方法的速度因子是3倍:

快速:myMap[key]++

慢:myMap[key]=myMap[key]+1

这可能不足为奇,因为至少在第二种情况下,我天真地指示Go两次访问myMap。我很好奇:熟悉Go编译器的人可以帮助我了解这些地图操作之间的区别吗?并且了解了编译器的工作原理之后,有没有更快的技巧来增加映射?

编辑:在本地运行该差异不明显,但仍然存在:

package main

import (
    "fmt"
    "math"
    "time"
)

func main() {

    x, y := make(map[int]int), make(map[int]int)
    x[0], y[0] = 0, 0
    steps := int(math.Pow(10, 9))

    start1 := time.Now()
    for i := 0; i < steps; i++ {
        x[0]++
    }
    elapsed1 := time.Since(start1)
    fmt.Println("++ took", elapsed1)

    start2 := time.Now()
    for i := 0; i < steps; i++ {
        y[0] = y[0] + 1
    }
    elapsed2 := time.Since(start2)

    fmt.Println("y=y+1 took", elapsed2)

}

输出:

++ took 8.1739809s
y=y+1 took 17.9079386s

Edit2:按照建议,我转储了机器代码。这是相关的片段

对于x [0] ++

0x4981e3              488d05b6830100          LEAQ runtime.types+95648(SB), AX
  0x4981ea              48890424                MOVQ AX, 0(SP)
  0x4981ee              488d8c2400020000        LEAQ 0x200(SP), CX
  0x4981f6              48894c2408              MOVQ CX, 0x8(SP)
  0x4981fb              48c744241000000000      MOVQ $0x0, 0x10(SP)
  0x498204              e8976df7ff              CALL runtime.mapassign_fast64(SB)
  0x498209              488b442418              MOVQ 0x18(SP), AX
  0x49820e              48ff00                  INCQ 0(AX)

对于y [0] = y [0] +1

0x498302              488d0597820100          LEAQ runtime.types+95648(SB), AX
  0x498309              48890424                MOVQ AX, 0(SP)
  0x49830d              488d8c24d0010000        LEAQ 0x1d0(SP), CX
  0x498315              48894c2408              MOVQ CX, 0x8(SP)
  0x49831a              48c744241000000000      MOVQ $0x0, 0x10(SP)
  0x498323              e80869f7ff              CALL runtime.mapaccess1_fast64(SB)
  0x498328              488b442418              MOVQ 0x18(SP), AX
  0x49832d              488b00                  MOVQ 0(AX), AX
  0x498330              4889442448              MOVQ AX, 0x48(SP)
  0x498335              488d0d64820100          LEAQ runtime.types+95648(SB), CX
  0x49833c              48890c24                MOVQ CX, 0(SP)
  0x498340              488d9424d0010000        LEAQ 0x1d0(SP), DX
  0x498348              4889542408              MOVQ DX, 0x8(SP)
  0x49834d              48c744241000000000      MOVQ $0x0, 0x10(SP)
  0x498356              e8456cf7ff              CALL runtime.mapassign_fast64(SB)
  0x49835b              488b442418              MOVQ 0x18(SP), AX
  0x498360              488b4c2448              MOVQ 0x48(SP), CX
  0x498365              48ffc1                  INCQ CX
  0x498368              488908                  MOVQ CX, 0(AX)

奇怪的是,++甚至没有调用地图访问权限! ++显然是一个简单的操作(大约2或3)。我解析机器的能力到此就结束了,因此,如果有人对正在发生的事情有深刻的了解,我很想听听

1 个答案:

答案 0 :(得分:4)

Go gc编译器是一种优化的编译器。它正在不断地得到改善。例如,对于Go1.11,

开始问题:cmd/compile: We can avoid extra mapaccess in "m[k] op= r" #23661

开始提交:7395083136539331537d46875ab9d196797a2173

cmd/compile: avoid extra mapaccess in "m[k] op= r"

Currently, order desugars map assignment operations like

    m[k] op= r

into

    m[k] = m[k] op r

which in turn is transformed during walk into:

    tmp := *mapaccess(m, k)
    tmp = tmp op r
    *mapassign(m, k) = tmp

However, this is suboptimal, as we could instead produce just:

    *mapassign(m, k) op= r

One complication though is if "r == 0", then "m[k] /= r" and "m[k] %=
r" will panic, and they need to do so *before* calling mapassign,
otherwise we may insert a new zero-value element into the map.

It would be spec compliant to just emit the "r != 0" check before
calling mapassign (see #23735), but currently these checks aren't
generated until SSA construction. For now, it's simpler to continue
desugaring /= and %= into two map indexing operations.

Fixes #23661.

代码结果:

go1.10

++ took 10.258130907s
y=y+1 took 10.233823639s

go1.11

++ took 7.995184419s
y=y+1 took 10.259916484s

您的问题的一般答案是在代码中简单,明确且显而易见。这样,编译器就可以轻松完成识别常见的可优化模式的任务。