环境:GO1.19.3,windows10
先说结论:
对于map[sting]* Object
1)如果结构体Object中含有指针或者string类型,GC耗时比较多,尤其是1000万条以后;应该使用bigCache等无GC组件存储;
2)如果Object中只有整形等,1亿条以下,性能比bigcache好很多;
之前看了一篇帖子,主要内容是讲大量的map中不要存储对象指针或者带指针的类型,会造成GC的代价比较大;
Go语言使用 map 时尽量不要在 big map 中保存指针 - 简书
- type urlInfo struct {
- url string
- Count uint64
- LastCount uint64
- }
-
- func MapWithPointer() {
- const N = 100000000
- m := make(map[string]*urlInfo)
- for i := 0; i < N; i++ {
- n := strconv.Itoa(i)
- info := urlInfo{n, uint64(i), uint64(i + 1)}
- m[n] = &info
- }
- now := time.Now()
- runtime.GC() // 手动触发 GC
- fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
-
- _ = m["0"] // 引用一下防止被 GC 回收掉
- }
我测试了一下,果然是这样的,当1000万条数据时,GC1秒多,如果是1亿条数据,GC会达到14秒,这样程序就完全不能用了!!!这时需要考虑使用freeCache和BigCache等组件;
但是,如果是不存储字符串的结构体,仅仅使用字符串作为键值,测试结果发现map还是可以用的,
我的需求是需要大概1000万个计数器,使用字符串作为键值,
计数器使用一个比较简单的类:
- type Counter struct {
- Count int64
- LastCount int64
- }
一、先简单的对比map[string]Counter 和map[string]*Counter的性能,
方法:
1)分别写入0-1000万编号为字符串的对象初始值;
2)随便找一个编号的对象做+1操作,执行700万次;计时;
3)手动执行GC操作,计时;
结果:
1)存对象:耗时82ms, GC 耗时: 10.0976ms
2)存指针:耗时62ms, GC 耗时: 10.0926ms
分析原因:map中对象无法直接更改,读写需要内存拷贝耗时更多,但是GC并没有发现使用指针消耗更多的资源;
备注:测试条目增加到1亿条数据后,发现存取无大影响,GC耗时140毫秒左右;
二、对比bigCache
如果使用BigCache:GitHub - allegro/bigcache: Efficient cache for gigabytes of data written in Go.
执行类似操作,执行700万次读写一个18字节的字节流,
结果:
操作耗时1592毫秒, GC 耗时: 11.0851ms
结论:在单线程情况下,bigCache并没有发现更大的优势;这里GC应该与bigCache存储的内容关系不大;
按照网上的测试,Go中的缓存现状(BigCache&FreeCache&GroupCache 缓存框架对比) - 简书
bigCache在40个并发的情况下性能有很大的提升:
但是,如果是性能差别如此明显的情况下,并发环境下我更倾向于对map的锁进行分片,比如concurrent-map,使用了一个泛型的map存储多个小map,每个小map分别加锁,真正存储数据。
GitHub - orcaman/concurrent-map: a thread-safe concurrent map for go
备注:有人写了一个帖子,使用gob执行对象的序列化,这样可以bigCache就可以读写任意类型的对象,但是:gob的性能非常差,并不可取,如果存储的类型比较简单完全可以自己编写序列化与反序列化函数,或者使用json或者使用protobuf,评测见:go语言序列化json/gob/msgp/protobuf性能对比 - 知乎
- func main() {
- testBigRaw()
- //MapWithPointer()
- //MapWithoutPointer()
- //testMap1()
- //testMap2()
- }
-
- func testBigRaw() {
- cache, _ := bigcache.NewBigCache(bigcache.Config{
- Shards: 16,
- LifeWindow: time.Second * 3600,
- CleanWindow: time.Hour * 24,
- MaxEntriesInWindow: 1000 * 10 * 60,
- MaxEntrySize: 500,
- Verbose: false,
- HardMaxCacheSize: 1024,
- StatsEnabled: true,
- })
-
- for i := 0; i < 10000000; i++ {
- key := strconv.Itoa(i)
- cache.Set(key, []byte("value1 and value2"))
- }
-
- timeUnixNano1 := time.Now().UnixMilli()
- // 100万次更新
- for i := 0; i < 7000000; i++ {
-
- cache.Get("111")
- cache.Set("111", []byte("value1 and value3"))
- }
- timeUnixNano2 := time.Now().UnixMilli()
- delta := timeUnixNano2 - timeUnixNano1
- fmt.Println(delta)
- now := time.Now()
- runtime.GC() // 手动触发 GC
- fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
-
- entry, _ := cache.Get("my-unique-key")
- fmt.Println(string(entry))
- }
-
- func testMap1() {
- cache := make(map[string]Counter)
- for i := 0; i < 10000000; i++ {
- key := strconv.Itoa(i)
- cache[key] = Counter{0, 0}
- }
-
- timeUnixNano1 := time.Now().UnixMilli()
- // 100万次更新
- for i := 0; i < 7000000; i++ {
-
- res, _ := cache["111"]
- res.Count += 1
- cache["111"] = res
- }
- timeUnixNano2 := time.Now().UnixMilli()
- delta := timeUnixNano2 - timeUnixNano1
- fmt.Println(delta)
- now := time.Now()
- runtime.GC() // 手动触发 GC
- fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
-
- }
-
- func testMap2() {
- cache := make(map[string]*Counter)
- for i := 0; i < 10000000; i++ {
- key := strconv.Itoa(i)
- cache[key] = &Counter{0, 0}
- }
-
- timeUnixNano1 := time.Now().UnixMilli()
- // 100万次更新
- for i := 0; i < 7000000; i++ {
-
- res, _ := cache["111"]
- res.Count += 1
- }
- timeUnixNano2 := time.Now().UnixMilli()
- delta := timeUnixNano2 - timeUnixNano1
- fmt.Println(delta)
-
- now := time.Now()
- runtime.GC() // 手动触发 GC
- fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
-
- }