V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
vcfghtyjc
V2EX  ›  Go 编程语言

有关 Golang 多线程对数组操作时的性能问题

  •  
  •   vcfghtyjc · 2020-01-05 03:30:02 +08:00 · 4411 次点击
    这是一个创建于 1545 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我尝试着用多线程分别运行以下两个函数,并使用 time 来查看它们的 cpu 使用率,一个的使用率就符合启动的线程数(比如 8 线程就得到 800% 的使用率),另一个就无法得到相符合的 cpu 使用率 (比如 8 线程得到 600% 的使用率,2 线程得到 300% 的使用率)。虽然两个函数都没有线程间的互斥锁,这两个函数的区别在于:第一个函数没有对数组的操作,第二个函数有对数组的操作。具体函数如下:

    函数 1: addOne

    该函数持续做加法运算

    func addOne(n int,wg *sync.WaitGroup){
        var i int
        for i < n{
            i++
        }
        wg.Done()
    }
    

    函数 2: appendArray

    该函数不断向一个数组中添加元素,为了防止内存溢出,每添加 10,000 个元素,就清空数组。

    func appendArray(n int,wg *sync.WaitGroup){
        var i int
        list := make([]int,10000)
        for i < n{
            for j := 0 ; j < 10000; j++{
                list = append(list, i)
            }
            list = make([]int,10000)
            i++
        }
        wg.Done()
    }
    

    启动多线程的函数: test

    该函数将工作总数 totalJobs 平分给每个线程

    func test(totalThreads, totalJobs int){
        n := totalJobs/totalThreads
        var wg sync.WaitGroup
        for i := 0; i < totalThreads; i++{
            wg.Add(1)
            go appendArray(n,&wg)
            // go addOne(n,&wg)
        }
        wg.Wait()
    }
    

    通过使用 pprof, 两个代码的 cpu 分析如下:

    函数 1: addOne

    函数 2: appendArray

    我发现对于第二个函数,虽然代码中没有调用锁,在执行的过程中会出现 runtime lock, runtime futex 之类的锁操作。

    这里我不明白的是:

    1. 为什么在有数组操作后,编译后的程序出现了锁操作,从而影响力 cpu 的使用?
    2. 如何避免 /减缓这些锁操作带来的性能影响?
    第 1 条附言  ·  2020-01-06 02:48:15 +08:00

    这里总结一下:

    这些锁操作的目的是为了 mem alloc 服务的,这包括了 makeappend 操作。如大家所说,当把 list = make([]int,10000) 变为 list = make([]int, 0, 10000) 后,运行速度是变快了数倍,但是cpu的使用率与之前差异却不大(16线程大概800%)。

    如果把函数变成对list中各个元素赋值(代码如下),而不调用 make,则 cpu 使用率是正常的。可见 make 申请新的空间也会出现锁操作。

    func modifyArray(n int,wg *sync.WaitGroup){
        list := make([]int,10000)
        for i:=0; i < n; i++{
            for j := 0 ; j < 10000; j++{
                list[j] = i
            }
            i++
        }
        wg.Done()
    }
    
    
    18 条回复    2020-01-10 21:51:05 +08:00
    nifury
        1
    nifury  
       2020-01-05 04:02:54 +08:00
    因为 append 做不到原子操作吧?
    ncwhale
        2
    ncwhale  
       2020-01-05 04:05:09 +08:00   ❤️ 1
    mem alloc 和 move 都是开销大头啊喵……没事别这么玩内存啊喵……碎片化和反复拷贝都是超级消耗资源的,而且,还因为内存 /缓存 /CPU 多核同步等问题,导致必须上锁否则会出现未知内存分布状态喵……

    正常一点做法就是别在循环里艹数组尺寸喵!提前预判一下分配空间就好!

    否则请使用其它数据结构而不是数组!
    mcrwayfun
        3
    mcrwayfun  
       2020-01-05 07:26:11 +08:00 via iPhone
    我想楼主想表达的是切片而不是数组
    gramyang
        4
    gramyang  
       2020-01-05 07:30:07 +08:00 via Android
    我测试过 append 是可以直接操作一个切片声明而不报空指针的,所以 append 里面会调用 make 返回一个切片实例。这种操作必然是耗时操作。
    不过题主用的这个 pprof 看起来挺好用的,可以试试
    Herobs
        5
    Herobs  
       2020-01-05 08:38:41 +08:00 via Android
    make 的第二个参数是长度,第三个参数才是容量。
    useben
        6
    useben  
       2020-01-05 09:04:25 +08:00
    make 的第二个参数是长度,第三个参数才是容量。+1
    你这里还是会指数分配内存
    nifury
        7
    nifury  
       2020-01-05 09:44:43 +08:00
    @mcrwayfun #3 啊瞎了。就扫了一眼没过大脑
    zhs227
        8
    zhs227  
       2020-01-05 09:47:04 +08:00   ❤️ 1
    append 很多情况下是对对内存进行 malloc, memcpy, free。这些操作未完成之前是不会返回的。所以你会看到各种锁。在有锁的状态下,其它的 goroutine 实际上是在等待。
    dazhangpan
        9
    dazhangpan  
       2020-01-05 10:34:14 +08:00
    用 time 查看 CPU 的使用率???
    Smash
        10
    Smash  
       2020-01-05 13:49:05 +08:00
    歪个楼,楼主函数调用图是用什么生成的?
    qieqie
        11
    qieqie  
       2020-01-05 14:24:11 +08:00 via iPhone
    @Smash pprof 然后用 svg 或者别的图片格式导出
    watsy0007
        12
    watsy0007  
       2020-01-05 14:36:26 +08:00
    ```golang
    func appendArray(n int,wg *sync.WaitGroup){
    var i int
    list := make([]int,10000)
    for i < n{
    for j := 0 ; j < 10000; j++{
    list[j] = i
    }
    list = make([]int,10000)
    i++
    }
    wg.Done()
    }
    ```
    whoami9894
        13
    whoami9894  
       2020-01-05 18:49:16 +08:00
    原因就是#5 说的,应该`make([]int, 0, 10000)`
    CEBBCAT
        14
    CEBBCAT  
       2020-01-05 19:22:22 +08:00
    鼓捣了大半天,被人家几小时之前就说过了,不爽啊……

    https://play.golang.org/p/3iTdWuMIgoe
    vcfghtyjc
        15
    vcfghtyjc  
    OP
       2020-01-06 02:27:02 +08:00
    @ncwhale 是否对于任何多线程程序都会出现这个问题(线程申请新的内存时,所有线程需要用锁来同步当前进程控制的内存)?如果用其他语言,如 C++,它的 overhead 还有这么高吗?
    vcfghtyjc
        16
    vcfghtyjc  
    OP
       2020-01-06 02:27:59 +08:00
    @dazhangpan 有什么推荐的工具吗?
    fcten
        17
    fcten  
       2020-01-06 10:31:56 +08:00
    内存分配是必然要加锁的,因为堆内存是整个程序共享的
    比较现代的内存分配器会对多线程场景进行优化,把一部分内存划分为线程独占,从而减少锁的使用。
    ncwhale
        18
    ncwhale  
       2020-01-10 21:51:05 +08:00
    @vcfghtyjc 这要看内存分配实现方式,比如 操作系统 对 进程 的堆分配必然会上锁互斥,但是如果是进程自己有 malloc / free 实现则可以在一定程度上缓解这类问题,比如 TCMalloc 一类工具。

    ——但是这不是写烂代码的理由喵!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1368 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 75ms · UTC 23:35 · PVG 07:35 · LAX 16:35 · JFK 19:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.