前面我们比较了 muduo 和 libevent2 的吞吐量,得到的结论是 muduo 比 libevent2 快 18%。有人会说,libevent2 并不是为高吞吐的应用场景而设计的,这样的比较不公平,胜之不武。为了公平起见,这回我们用 libevent2 自带的性能测试程序(击鼓传花)来对比 muduo 和 libevent2 在高并发情况下的 IO 事件处理效率。
测试用的软硬件环境与《muduo 与 boost asio 吞吐量对比》和《muduo 与 libevent2 吞吐量对比》相同,另外我还在自己的笔记本上运行了测试,结果也附在后面。
测试的场景是:有 1000 个人围成一圈,玩击鼓传花的游戏,一开始第 1 个人手里有花,他把花传给右手边的人,那个人再继续把花传给右手边的人,当花转手 100 次之后游戏停止,记录从开始到结束的时间。
用程序表达是,有 1000 个网络连接 (socketpairs 或 pipes),数据在这些连接中顺次传递,一开始往第 1 个连接里写 1 个字节,然后从这个连接的另一头读出这 1 个字节,再写入第 2 个连接,然后读出来继续写到第 3 个连接,直到一共写了 100 次之后程序停止,记录所用的时间。
以上是只有一个活动连接的场景,我们实际测试的是 100 个或 1000 个活动连接(即 100 朵花或 1000 朵花,均匀分散在人群手中),而连接总数(即并发数)从 100 到 100,000 (十万)。注意每个连接是两个文件描述符,为了运行测试,需要调高每个进程能打开的文件数,比如设为 256000。
libevent2 的测试代码位于 test/bench.c,我修复了 2.0.6-rc 版里的一个小 bug,修正后的代码见 http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/bench.c
muduo 的测试代码位于 examples/pingpong/bench.cc,见 http://gist.github.com/564985#file_pingpong_bench.cc
第一轮,分别用 100 个活动连接和 1000 个活动连接,无超时,读写 100 次,测试一次游戏的总时间(包含初始化)和事件处理的时间(不包含注册 event watcher)随连接数(并发数)变化的情况。具体解释见 libev 的性能测试文档 http://libev.schmorp.de/bench.html ,不同之处在于我们不比较 timer event 的性能,只比较 IO event 的性能。对每个并发数,程序循环 25 次,刨去第一次的热身数据,后 24 次算平均值。测试用的脚本在 http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh 。这个脚本是 libev 的作者 Marc Lehmann 写的,我略作改用,用于测试 muduo 和 libevent2。
第一轮的结果,请先只看红线和绿线。红线是 libevent2 用的时间,绿线是 muduo 用的时间。数字越小越好。注意这个图的横坐标是对数的,每一个数量级的取值点为 1, 2, 3, 4, 5, 6, 7.5, 10。
从红绿线对比可以看出:
1. libevent2 在初始化 event watcher 上面比 muduo 快 20% (左边的两个图)
2. 在事件处理方面(右边的两个图):a) 在 100 个活动连接的情况下,libevent2 和 muduo 分段领先。当总连接数(并发数)小于 1000 时,二者性能差不多;当总连接数大于 30000 时,muduo 略占优;当总连接数大于 1000 小于 30000 时,libevent2 明显领先。b) 在 1000 个活动连接的情况下,当并发数小于 10000 时,libevent2 和 muduo 得分接近;当并发数大于 10000 时,muduo 明显占优。
这里我们有两个问题:1. 为什么 muduo 花在初始化上的时间比较多? 2. 为什么在一些情况下它比 libevent2 慢很多。
我仔细分析了其中的原因,并参考了 libev 的作者 Marc Lehmann 的观点 ( http://lists.schmorp.de/pipermail/libev/2010q2/001041.html ),结论是:在第一轮初始化时,libevent2 和 muduo 都是用 epoll_ctl(fd, EPOLL_CTL_ADD, …) 来添加 fd 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,并忽略返回的错误。
第二轮测试结果见上图的蓝线,可见改动之后的 muduo 的初始化性能比 libevent2 更好,事件处理的耗时也有所降低(我推测是 kernel 内部的原因)。
这个改动只是为了验证想法,我并没有把它放到 muduo 最终的代码中去,这或许可以留作日后优化的余地。(具体的改动是 muduo/net/poller/EPollPoller.cc 第 115 行和 144 行,读者可自行验证。)
同样的测试在双核笔记本电脑上运行了一次,结果如下:(我的笔记本的 CPU 主频是 2.4GHz,高于台式机的 1.86GHz,所以用时较少。)
结论:在事件处理效率方面,muduo 与 libevent2 总体比较接近,各擅胜场。在并发量特别大的情况下(大于 10k),muduo 略微占优。
关于 muduo 的更多介绍请见《发布一个基于 Reactor 模式的 C++ 网络库》。muduo 的项目网站是 http://code.google.com/p/muduo ,上面有个 class diagram 可供参考。