稀疏矩阵排序代码单核运行效率高,采用多进程并行后性能暴跌的原因是什么?
并行执行后性能远低于单核的核心原因
大对象全量拷贝的通信开销(最大的性能黑洞)
你用的joblib并行默认走loky跨平台多进程后端,为了兼容Windows的进程启动限制,所有工作进程都是完全隔离的独立内存空间,根本不会自动和主进程共享内存:
- 你的
sort_rows函数里引用了两个占内存的大对象:存了全量相似度的稀疏CSR矩阵cosine_sim、长度40万的名称数组names,这类对象随随便便就能占到几百MB内存。 - 多进程刚启动的时候,主进程会把这两个大对象用pickle序列化,完完整整拷一遍到每个工作进程的独立内存里。要是开8个工作进程,光这一步要传输的内存流量就有好几GB,花的时间早就超过你单线程跑8秒的总计算时长了。
- 这种独立进程的内存拷贝还会打乱CPU缓存,严重的时候甚至会触发系统内存交换,把每个进程的实际计算速度拖得更慢。
任务切太碎带来的无意义调度开销
你生成并行任务的逻辑是按固定batch切分行索引,要是batch值设得太小,会生成一大堆细碎的并行任务:
- 每提交一个任务,joblib都得走一遍参数序列化、任务分发、结果回传、反序列化的流程。要是总任务数上千,这些固定的调度开销加起来,会远超过每个任务本身的实际计算时间。
- 最后拼结果的时候,你得把每个进程返回的嵌套列表全量传回到主进程,再用
np.vstack拼起来,这部分结果传输的开销还会再叠一层。
原本的冗余计算在多进程环境下被放大
你单线程的代码里本来就有很多可以靠CSR矩阵原生结构省掉的无用操作:
- 你逐行调
getrow(i).toarray()的时候,本来只存非零值的稀疏行,会被转换成和全矩阵宽度一样长的密集数组,再用np.nonzero找非零位置。但CSR矩阵本身就靠indptr、indices、data三个内置数组存好了每行的非零索引和对应的值,这部分转数组的操作,本来就是你单线程跑8秒的主要耗时来源。 - 这些临时生成的密集数组没法在多进程之间共享,每个进程都得重复生成一遍,进一步占内存、拖速度。
可参考的优化方向
- 干掉大对象的重复拷贝:Linux环境下可以把joblib的运行后端切成fork模式,直接复用主进程的内存空间读
cosine_sim和names;或者用joblib的共享内存接口把大对象映射成多进程共享的只读内存段,完全绕开序列化拷贝的流程。 - 把任务粒度调大:让每个工作进程处理的行数提到千行级别,减少总任务数,让真正的计算时长占比远高于调度通信的固定开销。
- 删掉冗余的数组转换操作:直接靠CSR矩阵的内置索引切片拿每行的非零索引和值,不用转成密集数组再找位置,本身就能把单线程耗时从8秒压到2-3秒,进一步降低并行的开销占比。
内容的提问来源于stack exchange,提问作者luky
相关产品推荐
相关产品推荐

