为何部分场景下Numba并行线程数增多时运行速度反而下降?
Numba并行分块矩阵求逆线程数超过阈值后耗时升高问题
问题描述
实现基于Numba并行的分块矩阵求逆功能时,测试发现线程数提升到一定程度后,继续增加线程数运行耗时不下降反而升高,例如8核16线程CPU上16线程耗时高于15线程。
核心实现代码
from numpy import empty,float64 from numpy.linalg import inv from numba import njit, prange,set_num_threads @njit(parallel=True) def tester1(A,m,n): inv_Lambda_p=empty((n,m,m),dtype=float64) for i in prange(n): inv_Lambda_p[i]=inv(ascontiguousarray(A[i]))
测试代码
A=np.random.rand(1000000,10,10) m=10 n=1000000 inv_a=tester1(A,m,n) for i in range (16): print("Thread ",i+1) set_num_threads(i+1) %timeit tester1(A,m,n)
测试结果
Thread 1 1.72 s ± 9.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 2 932 ms ± 32.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 3 680 ms ± 15.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 4 573 ms ± 13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 5 555 ms ± 8.73 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 6 530 ms ± 4.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 7 511 ms ± 6.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 8 492 ms ± 4.15 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 9 478 ms ± 5.19 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 10 471 ms ± 4.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 11 498 ms ± 4.75 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 12 451 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 13 442 ms ± 2.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 14 431 ms ± 3.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 15 421 ms ± 3.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) Thread 16 442 ms ± 4.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
测试硬件配置:8核16线程CPU,主频3.7GHz,16MB缓存,16GB内存,水冷散热。
结论
这个现象完全正常,是x86架构CPU运行计算密集型任务时的典型表现。
核心成因
- 超线程的资源共享限制:8物理核16线程的CPU,超线程技术是让两个逻辑线程共享同一个物理核的浮点运算单元、缓存、指令译码器等硬件资源。10x10小矩阵求逆属于高运算密度任务,单线程运行时就能占满单个物理核的大部分浮点算力,当线程数超过8个开始占用同物理核的第二个逻辑线程时,新增线程拿不到独立的运算资源,只会和同核线程争抢资源,性能增益快速收窄,甚至出现负增长。
- 内存带宽与缓存争抢:测试用的输入数组总大小约800MB,远大于CPU的16MB L3缓存,所有线程都需要频繁从主内存读取数据。线程数超过物理核数量后,多个线程争抢内存带宽通道、缓存行的概率陡增,缓存命中率下降,内存访问延迟升高,这部分开销会直接抵消并行带来的收益。测试结果中11线程、16线程的耗时反弹,就是争抢开销超过并行收益的直接表现。
- 并行调度开销占比升高:Numba的
prange底层基于线程池实现任务调度,场景中每个子任务只是计算一个10x10矩阵的逆,单次执行耗时极短。当线程数过多,线程唤醒、任务分发、结果同步的固定开销占总运行时间的比例会明显上升,线程数越多这部分开销越高。 - 嵌套并行的线程过度订阅:Numba内部的
inv函数底层调用OpenBLAS/MKL等线性代数库,这些库默认会开启内部多线程。如果没有手动关闭BLAS的内部多线程,会出现外层Numba开多线程、内层BLAS也开多线程的嵌套并行问题,线程总数远超CPU硬件承载能力,调度冲突开销在线程数超过物理核后会陡增。
优化建议
- 线程数优先设置为和物理核数一致即8线程,最高不要超过12线程,避免触发严重的硬件资源争抢。
- 运行代码前设置环境变量关闭BLAS库的内部多线程,避免嵌套并行:
export OPENBLAS_NUM_THREADS=1 export MKL_NUM_THREADS=1 - 删除循环中多余的
ascontiguousarray调用,通过np.random.rand生成的数组本身就是C内存连续布局,该调用会产生不必要的内存检查甚至拷贝开销。
内容的提问来源于stack exchange,提问作者ABZANMASTER
相关产品推荐
相关产品推荐

