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

2020-01-05 03:30:02 +08:00
 vcfghtyjc

我尝试着用多线程分别运行以下两个函数,并使用 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:

函数 2:

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

这里我不明白的是:

  1. 为什么在有数组操作后,编译后的程序出现了锁操作,从而影响力 cpu 的使用?
  2. 如何避免 /减缓这些锁操作带来的性能影响?
4443 次点击
所在节点    Go 编程语言
18 条回复
nifury
2020-01-05 04:02:54 +08:00
因为 append 做不到原子操作吧?
ncwhale
2020-01-05 04:05:09 +08:00
mem alloc 和 move 都是开销大头啊喵……没事别这么玩内存啊喵……碎片化和反复拷贝都是超级消耗资源的,而且,还因为内存 /缓存 /CPU 多核同步等问题,导致必须上锁否则会出现未知内存分布状态喵……

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

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

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

——但是这不是写烂代码的理由喵!

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/635143

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX