• golang单线程对比map与bigCache小对象存取性能差别


    环境:GO1.19.3,windows10

    先说结论:

    对于map[sting]* Object

    1)如果结构体Object中含有指针或者string类型,GC耗时比较多,尤其是1000万条以后;应该使用bigCache等无GC组件存储;

    2)如果Object中只有整形等,1亿条以下,性能比bigcache好很多;

    之前看了一篇帖子,主要内容是讲大量的map中不要存储对象指针或者带指针的类型,会造成GC的代价比较大;

    Go语言使用 map 时尽量不要在 big map 中保存指针 - 简书

    1. type urlInfo struct {
    2. url string
    3. Count uint64
    4. LastCount uint64
    5. }
    6. func MapWithPointer() {
    7. const N = 100000000
    8. m := make(map[string]*urlInfo)
    9. for i := 0; i < N; i++ {
    10. n := strconv.Itoa(i)
    11. info := urlInfo{n, uint64(i), uint64(i + 1)}
    12. m[n] = &info
    13. }
    14. now := time.Now()
    15. runtime.GC() // 手动触发 GC
    16. fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
    17. _ = m["0"] // 引用一下防止被 GC 回收掉
    18. }

    我测试了一下,果然是这样的,当1000万条数据时,GC1秒多,如果是1亿条数据,GC会达到14秒,这样程序就完全不能用了!!!这时需要考虑使用freeCache和BigCache等组件;

    但是,如果是不存储字符串的结构体,仅仅使用字符串作为键值,测试结果发现map还是可以用的,

    我的需求是需要大概1000万个计数器,使用字符串作为键值,

    计数器使用一个比较简单的类:

    1. type Counter struct {
    2. Count int64
    3. LastCount int64
    4. }

    一、先简单的对比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性能对比 - 知乎

    1. func main() {
    2. testBigRaw()
    3. //MapWithPointer()
    4. //MapWithoutPointer()
    5. //testMap1()
    6. //testMap2()
    7. }
    8. func testBigRaw() {
    9. cache, _ := bigcache.NewBigCache(bigcache.Config{
    10. Shards: 16,
    11. LifeWindow: time.Second * 3600,
    12. CleanWindow: time.Hour * 24,
    13. MaxEntriesInWindow: 1000 * 10 * 60,
    14. MaxEntrySize: 500,
    15. Verbose: false,
    16. HardMaxCacheSize: 1024,
    17. StatsEnabled: true,
    18. })
    19. for i := 0; i < 10000000; i++ {
    20. key := strconv.Itoa(i)
    21. cache.Set(key, []byte("value1 and value2"))
    22. }
    23. timeUnixNano1 := time.Now().UnixMilli()
    24. // 100万次更新
    25. for i := 0; i < 7000000; i++ {
    26. cache.Get("111")
    27. cache.Set("111", []byte("value1 and value3"))
    28. }
    29. timeUnixNano2 := time.Now().UnixMilli()
    30. delta := timeUnixNano2 - timeUnixNano1
    31. fmt.Println(delta)
    32. now := time.Now()
    33. runtime.GC() // 手动触发 GC
    34. fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
    35. entry, _ := cache.Get("my-unique-key")
    36. fmt.Println(string(entry))
    37. }
    38. func testMap1() {
    39. cache := make(map[string]Counter)
    40. for i := 0; i < 10000000; i++ {
    41. key := strconv.Itoa(i)
    42. cache[key] = Counter{0, 0}
    43. }
    44. timeUnixNano1 := time.Now().UnixMilli()
    45. // 100万次更新
    46. for i := 0; i < 7000000; i++ {
    47. res, _ := cache["111"]
    48. res.Count += 1
    49. cache["111"] = res
    50. }
    51. timeUnixNano2 := time.Now().UnixMilli()
    52. delta := timeUnixNano2 - timeUnixNano1
    53. fmt.Println(delta)
    54. now := time.Now()
    55. runtime.GC() // 手动触发 GC
    56. fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
    57. }
    58. func testMap2() {
    59. cache := make(map[string]*Counter)
    60. for i := 0; i < 10000000; i++ {
    61. key := strconv.Itoa(i)
    62. cache[key] = &Counter{0, 0}
    63. }
    64. timeUnixNano1 := time.Now().UnixMilli()
    65. // 100万次更新
    66. for i := 0; i < 7000000; i++ {
    67. res, _ := cache["111"]
    68. res.Count += 1
    69. }
    70. timeUnixNano2 := time.Now().UnixMilli()
    71. delta := timeUnixNano2 - timeUnixNano1
    72. fmt.Println(delta)
    73. now := time.Now()
    74. runtime.GC() // 手动触发 GC
    75. fmt.Printf("With a map of strings, GC took: %s\n", time.Since(now))
    76. }

  • 相关阅读:
    python脚本实现全景站点欧拉角转矩阵
    Java SpringBoot 加载 yml 配置文件中字典项
    OpenCV入门(C++/Python)- 使用OpenCV标注图像(六)
    micronet ICCV2021
    推荐算法——自动特征交叉
    Geteway
    Docker 安装Redis 无法使用配置文件设置密码问题
    Eigen C++之Eigen库基本使用(下)
    应用缺少POI数据,如何开发地点深度信息?
    ASP.NET Core Web API下基于Keycloak的多租户用户授权的实现
  • 原文地址:https://blog.csdn.net/robinfoxnan/article/details/127730428