6.5.2 击鼓传花:对比muduo 与libevent2 的事件处理效率
前面我们比较了muduo 和libevent2 的吞吐量, 得到的结论是muduo 比libevent2 快18%. 有人会说,libevent2 并不是为高吞吐量的应用场景而设计的,这样的比较不公平,胜之不武。为了公平起见,这回我们用libevent2 自带的性能测试程序(击鼓传花)来对比muduo 和libevent2 在高并发情况下的IO 事件处理效率。
测试用的软硬件环境与前一小节相同,另外我还在自己的DELL E6400 笔记本电脑上运行了测试,结果也附在后面。
测试的场景是:有1000 个人围成一圈,玩击鼓传花的游戏,一开始第1 个人手里有花,他把花传给右手边的人,那个人再继续把花传给右手边的人,当花转手100次之后游戏停止,记录从开始到结束的时间。
用程序表达是,有1000 个网络连接(socketpair(2) 或pipe(2)),数据在这些连接中顺次传递,一开始往第1 个连接里写1 个字节,然后从这个连接的另一头读出这1 个字节,再写入第2 个连接,然后读出来继续写到第3 个连接,直到一共写了100 次之后程序停止,记录所用的时间。
以上是只有一个活动连接的场景,我们实际测试的是100 个或1000 个活动连接(即100 朵花或1000 朵花,均匀分散在人群手中),而连接总数(即并发数)从100 ~100 000(10 万)。注意每个连接是两个文件描述符,为了运行测试,需要调高每个进程能打开的文件数,比如设为256 000。
libevent2 的测试代码位于test/bench.c,我修复了2.0.6-rc 版里的一个小bug。修正后的代码见已经提交给libevent2 作者,现在下载的最新版本是正确的。
muduo 的测试代码位于examples/pingpong/bench.cc。
测试结果与讨论
第一轮,分别用100 个活动连接和1000 个活动连接,无超时,读写100 次,测试一次游戏的总时间(包含初始化)和事件处理的时间(不包含注册event watcher)随连接数(并发数)变化的情况。具体解释见libev 的性能测试文档19,不同之处在于我们不比较timer event 的性能,只比较IO event 的性能。对每个并发数,程序循环25 次,刨去第一次的热身数据,后24 次算平均值。测试用的脚本20 是libev 的作者Marc Lehmann 写的,我略做改用,用于测试muduo 和libevent2。
第一轮的结果(见图6-6),请先只看“+”线(实线)和“”线(粗虚线)。“”线是libevent2 用的时间,“+”线是muduo 用的时间。数字越小越好。注意这个图的横坐标是对数的,每一个数量级的取值点为1,2,3,4,5,6,7.5,10。
从两条线的对比可以看出:
1. libevent2 在初始化event watcher 方面比muduo 快20% (左边的两个图)。
2. 在事件处理方面(右边的两个图)
a. 在100 个活动连接的情况下,
当总连接数(并发数)小于1000 或大于30 000 时,二者性能差不多;
当总连接数大于1000 或小于30 000 时,libevent2 明显领先。
b. 在1000 个活动连接的情况下,
当并发数小于10 000 时,libevent2 和muduo 得分接近;
当并发数大于10 000 时,muduo 明显占优。
这里有两个问题值得探讨:
1. 为什么muduo 花在初始化上的时间比较多?
2. 为什么在一些情况下它比libevent2 慢很多?
我仔细分析了其中的原因,并参考了libev 的作者Marc Lehmann 的观点21,结论是:在第一轮初始化时,libevent2 和muduo 都是用epoll_ctl(fd, EPOLL_CTL_ADD, ...) 来添加文件描述符的event watcher。不同之处在于,在后面24 轮中,muduo 使用了epoll_ctl(fd, EPOLL_CTL_MOD, ...) 来更新已有的event watcher;然而libevent2 继续调用epoll_ctl(fd, EPOLL_CTL_ADD, ...) 来重复添加fd,并忽略返回的错误码EEXIST (File exists)。在这种重复添加的情况下,EPOLL_CTL_ADD将会快速地返回错误,而EPOLL_CTL_MOD 会做更多的工作,花的时间也更长。于是libevent2 捡了个便宜。
为了验证这个结论,我改动了muduo,让它每次都用EPOLL_CTL_ADD 方式初始化和更新event watcher,并忽略返回的错误。
第二轮测试结果见图6-6 的细虚线,可见改动之后的muduo 的初始化性能比libevent2 更好,事件处理的耗时也有所降低(我推测是kernel 内部的原因)。
这个改动只是为了验证想法,我并没有把它放到muduo 最终的代码中去,这或许可以留作日后优化的余地。(具体的改动是muduo/net/poller/EPollPoller.cc 第138 行和173 行,读者可自行验证。)
同样的测试在双核笔记本电脑上运行了一次,结果如图6-7 所示。(我的笔记本电脑的CPU 主频是2.4 GHz,高于台式机的1.86 GHz,所以用时较少。)
结论:在事件处理效率方面,muduo 与libevent2 总体比较接近,各擅胜场。在并发量特别大的情况下(大于10 000),muduo 略微占优。