内容简介:libcopp(v2) vs goroutine性能测试
本来是没想写这个对比。无奈之前和 call_in_stack 的作者聊了一阵,发现了一些 libcopp 的改进空间。然后顺便看了新的boost.context的cc部分的代码,有所启发。想给 libcopp 做一些优化,主要集中在减少分配次数从而减少内存碎片;在支持的编译器里有些地方用右值引用来减少不必要的拷贝;减少原子操作和减少L1cache miss几个方面。
之后改造了茫茫多流程和接口后出了v2版本,虽然没完全优化完,但是组织结构已经定型了,可以用来做压力测试。为了以后方便顺便还把cppcheck和clang-analyzer的静态分析 工具 写进了dev脚本。然后万万没想到的是,在大量协程的情况下,benchmark的结果性能居然比原来还下降了大约1/3。
我结合valgrind和perf的报告分析了一下原因,原来的读L1 cache miss大约在68%左右,而v2里的读L1 cache miss到了98%。究其原因,原来的协程、协程任务对象和协程任务的actor是由malloc分配的,而现在在v2里全部优化后放进了分配的执行栈里。这样减少了三次malloc操作。但是这也导致不同协程、任务和actor之间的距离隔得非常远,必超出L1 cache的量(一般是64字节)。那么就必然容易L1 cache miss了。而原来在benchmark里由于是连续分配的,所以他们互相都在比较近的位置,当然原来的性能高了。
这种情况,因该说是原来的benchmark更加不能作为实际使用过程中的性能参考依据。之前也说过,因为在实际应用场景中几乎必然cache miss,因为逻辑会更复杂得多,内存访问也更切换得频繁和多。
这让我突然想到了 go 语言的goroutine。不知道这玩意有没有考虑切换时的Cache miss开销,不知道针对这个做优化。因为它是语言级别实现的 go 程,那么如果要针对缓存做优化则比较容易实现一些。不过它的动态栈总归会有一定的开销。
goroutine压力测试
这里还是用了和 libcopp 里差不多的测试方法。benchmark的代码如下:
package main import ( "fmt" "os" "strconv" "time" ) func runCallback(in, out chan int64) { for n, ok := <-in; ok; n, ok = <-in { out <- n } } func runTest(round int, coroutineNum, switchTimes int64) { fmt.Printf("##### Round: %v\n", round) start := time.Now() channelsIn, channelsOut := make([]chan int64, coroutineNum), make([]chan int64, coroutineNum) for i := int64(0); i < coroutineNum; i++ { channelsIn[i] = make(chan int64, 1) channelsOut[i] = make(chan int64, 1) } end := time.Now() fmt.Printf("Create %v goroutines and channels cost %vns, avg %vns\n", coroutineNum, end.Sub(start).Nanoseconds(), end.Sub(start).Nanoseconds()/coroutineNum) start = time.Now() for i := int64(0); i < coroutineNum; i++ { go runCallback(channelsIn[i], channelsOut[i]) } end = time.Now() fmt.Printf("Start %v goroutines and channels cost %vns, avg %vns\n", coroutineNum, end.Sub(start).Nanoseconds(), end.Sub(start).Nanoseconds()/coroutineNum) var sum int64 = 0 start = time.Now() for i := int64(0); i < switchTimes; i++ { for j := int64(0); j < coroutineNum; j++ { channelsIn[j] <- 1 sum += <-channelsOut[j] } } end = time.Now() fmt.Printf("Switch %v goroutines for %v times cost %vns, avg %vns\n", coroutineNum, sum, end.Sub(start).Nanoseconds(), end.Sub(start).Nanoseconds()/sum) start = time.Now() for i := int64(0); i < coroutineNum; i++ { close(channelsIn[i]) close(channelsOut[i]) } end = time.Now() fmt.Printf("Close %v goroutines cost %vns, avg %vns\n", coroutineNum, end.Sub(start).Nanoseconds(), end.Sub(start).Nanoseconds()/coroutineNum) } func main() { var coroutineNum, switchTimes int64 = 30000, 1000 fmt.Printf("### Run: ") for _, v := range os.Args { fmt.Printf(" \"%s\"", v) } fmt.Printf("\n") if (len(os.Args)) > 1 { v, _ := strconv.Atoi(os.Args[1]) coroutineNum = int64(v) } if (len(os.Args)) > 2 { v, _ := strconv.Atoi(os.Args[2]) switchTimes = int64(v) } for i := 1; i <= 5; i++ { runTest(i, coroutineNum, switchTimes) } }
同时发布在了: https://gist.github.com/owt5008137/2286768f2586521600c9fd1700cbf845
测试结果如下:
PS D:\projs\test\go> .\test_goroutine.exe ### Run: "D:\projs\test\go\test_goroutine.exe" ##### Round: 1 Create 30000 goroutines and channels cost 6515200ns, avg 217ns Start 30000 goroutines and channels cost 79505000ns, avg 2650ns Switch 30000 goroutines for 30000000 times cost 42225426300ns, avg 1407ns Close 30000 goroutines cost 15017500ns, avg 500ns ##### Round: 2 Create 30000 goroutines and channels cost 19868200ns, avg 662ns Start 30000 goroutines and channels cost 22487700ns, avg 749ns Switch 30000 goroutines for 30000000 times cost 44709165100ns, avg 1490ns Close 30000 goroutines cost 15559000ns, avg 518ns ##### Round: 3 Create 30000 goroutines and channels cost 3999700ns, avg 133ns Start 30000 goroutines and channels cost 17508400ns, avg 583ns Switch 30000 goroutines for 30000000 times cost 50535999000ns, avg 1684ns Close 30000 goroutines cost 36289900ns, avg 1209ns ##### Round: 4 Create 30000 goroutines and channels cost 5999600ns, avg 199ns Start 30000 goroutines and channels cost 44500300ns, avg 1483ns Switch 30000 goroutines for 30000000 times cost 45678842800ns, avg 1522ns Close 30000 goroutines cost 13005600ns, avg 433ns ##### Round: 5 Create 30000 goroutines and channels cost 5000000ns, avg 166ns Start 30000 goroutines and channels cost 14001000ns, avg 466ns Switch 30000 goroutines for 30000000 times cost 47485810100ns, avg 1582ns Close 30000 goroutines cost 17999800ns, avg 599ns
这里都是在我家里的Windows机器下跑的结果,在 Linux 下应该性能能够更好一些,因为我家里的机器比较渣,并且 libcopp 在Linux下性能就比在Windows下好得多。那么为了对比,还需要同样在这台机器下,同样环境的 libcopp 的测试结果。
这里用的是go语言推荐的协程间共享数据的方式,应该是最贴近 libcopp 的流程了。这里面可以看出来创建chan需要的开销并不大,但是其实goroutine的切换开销还是蛮大的,基本上都要超过1us。而且感觉go语言内部还是维护了goroutine的池子,不然创建开销抖动不会那么大。
不过goroutine的内存开销确实小,30000个goroutine的内存占用才300MB。
libcopp 的同环境报告和对比
我只贴一样的协程数量和切换次数的结果了
###################### task (stack using stack pool) ################### ########## Cmd: .\sample_benchmark_task_stack_pool.exe 30000 1000 64 ### Round: 1 ### create 30000 task, cost time: 0 s, clock time: 104 ms, avg: 3466 ns switch 30000 tasks 30000000 times, cost time: 18 s, clock time: 18500 ms, avg: 616 ns remove 30000 tasks, cost time: 0 s, clock time: 28 ms, avg: 933 ns ### Round: 2 ### create 30000 task, cost time: 0 s, clock time: 44 ms, avg: 1466 ns switch 30000 tasks 30000000 times, cost time: 19 s, clock time: 18341 ms, avg: 611 ns remove 30000 tasks, cost time: 0 s, clock time: 29 ms, avg: 966 ns ### Round: 3 ### create 30000 task, cost time: 0 s, clock time: 44 ms, avg: 1466 ns switch 30000 tasks 30000000 times, cost time: 18 s, clock time: 18188 ms, avg: 606 ns remove 30000 tasks, cost time: 0 s, clock time: 28 ms, avg: 933 ns ### Round: 4 ### create 30000 task, cost time: 0 s, clock time: 44 ms, avg: 1466 ns switch 30000 tasks 30000000 times, cost time: 18 s, clock time: 18267 ms, avg: 608 ns remove 30000 tasks, cost time: 0 s, clock time: 28 ms, avg: 933 ns ### Round: 5 ### create 30000 task, cost time: 0 s, clock time: 44 ms, avg: 1466 ns switch 30000 tasks 30000000 times, cost time: 19 s, clock time: 18772 ms, avg: 625 ns remove 30000 tasks, cost time: 0 s, clock time: 26 ms, avg: 866 ns
同样对比下, libcopp 的切换开销就小的多了,而且比较稳定,但是创建开销也是比较大,特别是第一次要分配栈的情况下(后面都会使用栈池机制,减少系统调用所以会小很多)。
其实这是v2的测试数据,虽然切换开销比原来是要大一些,但是之前在Linux上的结果,这个创建开销已经是原来版本的一半了(Linux上的创建开销原先大约是1us,v2大约是500ns,切换开销忘记了,v2版本大约是300-400ns)。
结论
go语言现在很火了,性能超过goroutine的话肯定是已经有实用价值了,特别是逻辑开销很容易就能抹平这个协程的开销。而且go语言本来还就是目标于高性能分布式系统的,并且很多这种分布式系统的一些逻辑可能并不特别重,都能容忍这个开销,何况 libcopp 呢。但是 libcopp 的v2版本细节上仍然还有一些优化点,比如内存布局和原子操作必导致L1 Cache失效的问题等等。等我一并处理完再merge回master。现在还是放在 https://github.com/owt5008137/libcopp 的v2分支里。
当然哪位大神有更好的建议希望能够不吝赐教。之前针对缓存优化的点,其实优化好了跑分会很好看,但是实用性上还得分场景。像 libcopp 的定位是比较重量级的场景,能够覆盖比较完整而且复杂的协程流程和逻辑,对于比较复杂的场景(比如我们游戏里),那些缓存优化就没太大意义。而对于那些简单的只是用来临时做上下文切换而且不要求跨平台跨编译器的,我还是建议使用类似 call_in_stack 这种轻量级的库,毕竟性能搞了一个数量级。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 微服务测试之性能测试
- Go 单元测试和性能测试
- 性能测试vs压力测试vs负载测试
- SpringBoot | 第十三章:测试相关(单元测试、性能测试)
- Golang 性能测试 (2) 性能分析
- 随行付微服务测试之性能测试 原 荐
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据结构 Python语言描述
[美] Kenneth A. Lambert 兰伯特 / 李军 / 人民邮电出版社 / 2017-12-1 / CNY 69.00
在计算机科学中,数据结构是一门进阶性课程,概念抽象,难度较大。Python语言的语法简单,交互性强。用Python来讲解数据结构等主题,比C语言等实现起来更为容易,更为清晰。 《数据结构 Python语言描述》第1章简单介绍了Python语言的基础知识和特性。第2章到第4章对抽象数据类型、数据结构、复杂度分析、数组和线性链表结构进行了详细介绍,第5章和第6章重点介绍了面向对象设计的相关知识、......一起来看看 《数据结构 Python语言描述》 这本书的介绍吧!