如题,众做周知 python 的(几乎)唯一痛点在于其孱弱的性能,所以写 py 脚本的人都会对代码性能优化非常敏感。这个贴算是分享一些今天测试下的小趣闻吧。
众所周知异步编程框架在一个事件循环下执行,我们普遍被教导如果用异步实现两个并行“线程”,相较于申请系统级线程,它有两头个优势 1、它并不需要在用户态与内核态之间切,理论性能更高 2、它的并行任务在同线程下执行,需要使用锁的情况很少 3、用户可以自由设置并行任务在何时跳出(将程序控制权限交还给事件循环),控制粒度更细致。
今天突发奇想想测试一下所谓的不需要进入内核态的线程切换,相比于传统模式到底有多少优势,简单一测,惊掉了下吧
以下是测试代码:
from collections import deque
from threading import Thread , Lock
import time , asyncio
def thread_speed_test():
def add1():
nonlocal count
for i in range(single_test_num):
mutex.acquire()
count += 1
mutex.release()
mutex = Lock()
count = 0
thread_list = list()
for i in range(thread_num):
thread_list.append(Thread(target = add1))
st_time = time.time()
for thr in thread_list:
thr.start()
for thr in thread_list:
thr.join()
ed_time = time.time()
print(count)
print(f'threading finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s')
def asyncio_speed_test():
count = 0
@asyncio.coroutine
def switch():
yield
async def add1():
nonlocal count
for i in range(single_test_num):
count += 1
await switch()
async def main():
tasks = asyncio.gather( *(add1() for i in range(thread_num))
)
st_time = time.time()
await tasks
ed_time = time.time()
print(count)
print(f'asyncio finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
if __name__ == "__main__":
single_test_num = 1000000
thread_num = 2
thread_speed_test()
asyncio_speed_test()
cpython3.7 运行结果如下:
2000000
threading finished in 0.9332s ,speed 2143159.1985q/s
2000000
asyncio finished in 16.044s ,speed 124657.3379q/s
简单来说就是做单纯的任务切换,让线程(或协程)交替运行,业务代码是将变量 count 每次加 1,加满 200 万次为止。 令人惊掉下巴的一点在于,根据测试结果,使用双线程交替执行代码的速度 [远高于] 使用协程安排代码,即便在额外增加线程锁的加锁与开锁的开销以后,其效率仍超出协程事件循环约 20 倍,说好的更快呢?
当然这个(也许)可以理解,原因大概在于系统角度看来线程切换在时间上的粒度较大,并不如 asyncio 是真正的两个协程交替,每次各自+1,直到加满 200 万次。线程的情况是,很可能在一次切换之间 forloop 已经运行出去几百上千次了,实际切换并不足两百万次。
实际表现就是,如果增大线程(或协程)数量,两个模型的效率一个会越来越快,一个会越来越慢 下面是一组逐渐增加线程数的测试:
# asyncio #
thread_num numbers of switching in 1sec average time of a single switch(ns)
2 122296 8176
32 243502 4106
128 252571 3959
512 253258 3948
4096 239334 4178
# threading #
thread_num numbers of switching in 1sec average time of a single switch(ns)
2 2278386 438
4 737829 1350
8 393786 2539
16 367123 2720
32 369260 2708
64 381061 2624
512 381403 2622
由此可见,随着线程逐渐增多,threading 的平均切换时间稳定在约 2600 纳秒,而 asyncio 稳定在 4000 纳秒左右。(但是线程还是比协程快啊 kora !)。虽然我们可以推测出可能导致这种情况的原因,并不足以推翻线程切换比协程切换更快的结论,但是这也确实地导向了一个疑问,即绝大多数情况下使用异步安排任务是否真的有意义呢?
=============================================================================
目前为止事情还处在可以理解的范畴内,而接下来的情况就有点匪夷所思了。
根据在 stackoverflow 上的老哥评论,该老哥没太搞清楚状况,他说代码中的await switch()
是干什么用的,可不可以注释掉,我注释掉以后提示在 0.1 秒内跑完了 200 万次,即每秒 2000 万次循环。
这个老哥很显然没有搞清楚状况,因为 switch()是一个生成器的封装,加入这行代码的意图在于将进程控制权交还给事件循环,如果注释掉的话代码就变成了普通的同步执行,事件循环会依次执行次数为一百万次的 forloop,直到指定个数为止。
这个可以理解,对于性能比较敏感的人而言,无法理解的一点是,cpython 解释器(比如我在用的 3.7 版,也是 asyncio api 稳定后的第一版),即使在同步的情况下,写一个简单的 forloop 并计算其运行时间,每秒也只能跑一千万轮左右。而这个老哥把它放到异步框架里跑,居然速度比同步运行提升了一倍。
大概只能用匪夷所思来形容了。诸位有兴趣可以自己测测。
1
crella 2020-03-10 12:08:06 +08:00 via Android
帮顶
|