You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 06:18:09