Python Numba并行线程数超5后耗时不降反升问题咨询
Numba并行线程数超过5后性能下降原因分析
测试基础信息
测试环境:8物理核心/16线程CPU,Anaconda环境下Jupyter Notebook
测试现象:njit开启并行模式后,线程数1-5区间耗时随线程数增加下降,线程数超过5后耗时随线程数增加逐步上升。
核心测试代码
from numba import njit,prange,set_num_threads,get_num_threads import numpy as np @njit(parallel=True) def test(x,y): z=np.empty((x.shape[0],x.shape[0]),dtype=np.float64) for i in prange(x.shape[0]): for j in range(x.shape[0]): z[i,j]=x[i,j]*y[i,j] return z
执行脚本
x=np.random.rand(10000,10000) y=np.random.rand(10000,10000) for i in range(16): set_num_threads(i+1) print("Number of threads :",get_num_threads()) %timeit -r 1 -n 10 test(x,y)
实测耗时数据
Number of threads : 1 234 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 2 178 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 3 168 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 4 161 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 5 148 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 6 152 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 7 152 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 8 153 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 9 154 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 10 156 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 11 158 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 12 157 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 13 158 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 14 160 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 15 160 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each) Number of threads : 16 161 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 10 loops each)
根本原因
这个现象是硬件层面的硬约束导致的,和Numba本身的编译、并行实现逻辑没有直接关系:
- 首要原因是内存带宽提前饱和:这个测试是典型的内存绑定型任务,而非计算绑定型任务。10000*10000规格的float64单矩阵大小为800MB,测试过程需要读取x、y两个矩阵共1.6GB数据,写入结果矩阵z共800MB数据,总内存数据吞吐量达2.4GB。5线程时总耗时148ms,折算实际内存带宽约16.2GB/s,已经达到普通消费级DDR4双通道内存的实际可用带宽上限。内存带宽的饱和是非线性的,不需要占满所有物理核心,只要核心发出的内存访问请求把内存控制器的处理队列打满,带宽就会摸到天花板,这也是性能拐点出现在5线程而非8物理核线程数的原因。此时继续增加线程,所有线程只会争抢有限的内存带宽,排队等待内存数据的耗时会直接抵消多线程带来的收益,最终表现为耗时上升。
- 超线程对纯计算密集任务无额外增益:设备的16线程是8个物理核心通过*超线程(SMT)*技术虚拟出的逻辑线程,同一个物理核心下的两个逻辑线程共享核心的浮点执行单元、缓存、内存总线接口。这个测试的核心操作是连续浮点乘法,单个线程运行时就已经把物理核心的浮点执行单元占满,同核心的第二个逻辑线程根本拿不到可用计算资源,反而会带来线程上下文切换、缓存争用的额外开销。
- 并行调度开销随线程数线性上升:Numba的
prange依赖底层线程池做循环分块调度,线程数越多,每个线程分到的循环块粒度越小,任务分发、线程同步、缓存一致性维护的开销占比就越高。当性能已经被内存带宽卡住时,这部分额外开销不会被计算收益抵消,会直接体现为总耗时上涨。
验证方法
可以通过两个简单测试复现验证上述结论:
- 将矩阵规模缩小到1000*1000,此时数据总量可以完全放进CPU L3缓存,不会触发内存带宽瓶颈,就能观察到线程数提升到8个物理核数前性能持续上涨,超过8核后性能基本持平。
- 将循环内的操作替换为计算量更大的逻辑(比如连续做10次以上乘加运算),把任务从内存绑定转为计算绑定,就能观察到8个物理核全部占满前性能持续提升,超过8核后性能不会明显上涨。
内容的提问来源于stack exchange,提问作者ABZANMASTER
相关产品推荐
相关产品推荐

