在Go中使用数组而不是微小的地图的原因及解决代码实例

86 阅读3分钟

今天我偶然发现了这个问题,所以我想我应该用更多的背景来描述一下眼前的问题。

我有一些文本数据,我想推断每个数据点是否是整数或浮点数等。我的数据类型看起来像这样。

type Dtype uint8

const (
	DtypeInvalid Dtype = iota
	DtypeNull
	DtypeString
	DtypeInt
	DtypeFloat
    DtypeBool
    // thanks to DtypeMax, I can add new types here without worrying about anything
	DtypeMax
)

注意DtypeMax ,这是我们成功的关键。但首先让我们生成一些数据

data := make([]Dtype, n)
for j := 0; j < n; j++ {
    data[j] = Dtype(rand.Intn(int(DtypeMax)))
}

一个直观的方法是计算每种类型的出现次数,就像这样做。

tgmap := make(map[Dtype]int)
for _, el := range data {
    // work to determine type
    tgmap[el]++
}

这个实施方案在几个月内都运行良好,但今天早些时候,我在想,毕竟填充地图的工作并不重要。我在重写时利用了两个因素。

  1. 我知道我有多少个类型,所以我可以创建一个数组--无需手动分配(不像切片或地图),在编译时就知道大小。
  2. 类型常量是数字的,所以我可以用它们作为这个数组的索引比哈希查找要便宜得多。
var tgarr [DtypeMax]int
for _, el := range data {
    // work to determine type
    tgarr[el]++
}

进展如何?我在每个条目上运行了10次,每次都有一百万个条目。我没有使用Go的测试包,因为我想在main() ,它通常足够用于性能差异大的小测试(我在这里不需要统计测试,否则我会使用testingbenchstat )。

took 31.441122ms to populate a map
took 29.453898ms to populate a map
took 29.518603ms to populate a map
took 29.254208ms to populate a map
took 31.35607ms to populate a map
took 28.851067ms to populate a map
took 29.209222ms to populate a map
took 29.355206ms to populate a map
took 29.451898ms to populate a map
took 30.235153ms to populate a map
took 521.01µs to populate an array
took 512.882µs to populate an array
took 516.915µs to populate an array
took 517.194µs to populate an array
took 541.786µs to populate an array
took 534.958µs to populate an array
took 525.162µs to populate an array
took 597.569µs to populate an array
took 534.92µs to populate an array
took 532.322µs to populate an array

因此,在不涉及紧缩循环的任何工作的情况下,我得到了60倍的速度提升。显然,这不会给你带来60倍的端到端加速,但取决于你的循环迭代有多昂贵,这可能是显著的(它给我带来了10-20%,这很好)。

启示:如果你有一个条目很少的地图,已知的域和受限的范围(即max-min是合理的),使用一个片断或数组可能非常有效,因为它的读/写成本更低。

整个代码在这里。

package main

import (
	"fmt"
	"log"
	"math/rand"
	"time"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

type Dtype uint8

const (
	DtypeInvalid Dtype = iota
	DtypeNull
	DtypeString
	DtypeInt
	DtypeFloat
	DtypeBool
	DtypeMax
)

func run() error {
	work := func() {
		time.Sleep(time.Nanosecond * 100)
	}
	_ = work

	n := 1000_000
	loops := 10
	data := make([]Dtype, n)
	for j := 0; j < n; j++ {
		data[j] = Dtype(rand.Intn(int(DtypeMax)))
	}

	for l := 0; l < loops; l++ {
		tgmap := make(map[Dtype]int)
		t := time.Now()
		for _, el := range data {
			// work()
			tgmap[el]++
		}
		fmt.Printf("took %v to populate a map\n", time.Since(t))
	}

	for l := 0; l < loops; l++ {
		var tgarr [DtypeMax]int
		t := time.Now()
		for _, el := range data {
			// work()
			tgarr[el]++
		}
		fmt.Printf("took %v to populate an array\n", time.Since(t))
	}

	return nil
}