Python多输入列表推导式并行化:osmnx最近节点查询提速问题咨询
并行实现性能不及原生版本的核心原因
- 大对象冗余传输开销:你当前的参数构造逻辑将完整的路网对象
G(通常带全量节点、边、属性,体积可达数百MB)塞进了每一个任务参数元组,每次任务调度时主进程都要重复对G做序列化、进程间传输。小样本测试时,这类固定开销远高于单条最近邻查询的毫秒级计算耗时,自然表现比原生列表推导差。 - 计时逻辑存在偏差:你将进程池创建、任务参数序列化、starmap_async触发的流程放在了
%%time计时块外部,实际测试只统计了结果获取阶段的等待时间,没有计入进程启动、大对象序列化的固定成本,进一步放大了并行版本的劣势。 - 任务粒度过细:单条
ox.get_nearest_node是依托本地空间索引的近邻查询,单次计算耗时极低,拆成每点一个独立任务的话,进程调度、通信的成本会完全抵消并行收益。 - 代码存在正确性bug:你此前写的坐标提取逻辑中,x、y列表都错误提取了质心的y坐标,后续实现时需要直接利用geopandas的向量化属性批量取质心坐标,避免逐行索引循环的冗余耗时。
可实现真实提效的并行方案
核心思路是让每个worker进程常驻持有一次路网对象,全程不再重复传输大体积的G,同时通过任务分块降低调度开销:
- 提前批量提取所有建筑的质心坐标,消除逐行循环的冗余计算
# 直接调用geopandas的向量化centroid属性,避免用for循环逐行取几何信息 centroids = buildings.geometry.centroid # 匹配osmnx要求的(纬度, 经度)输入格式构造坐标列表 query_points = list(zip(centroids.y, centroids.x)) - 利用进程池的初始化机制,让每个worker进程启动时只加载一次路网对象,从根源上消除重复传参的开销
import osmnx as ox import multiprocessing as mp from tqdm.notebook import tqdm # 定义worker进程内的全局存储变量 road_graph = None def _init_worker(graph): # 每个worker进程启动时仅执行一次初始化,将路网对象存在进程本地内存 global road_graph road_graph = graph def _get_nearest(point): # 实际执行查询的任务函数,仅传入坐标点,不再传输大对象 global road_graph return ox.get_nearest_node(road_graph, point) - 启动进程池时传入初始化参数,搭配分块调度降低通信频率
if __name__ == "__main__": # 适配macOS、Windows默认的spawn进程启动模式 # worker数量取CPU核心数减1即可,避免过度竞争内存和CPU资源 worker_count = mp.cpu_count() - 1 # 计算合适的分块大小,减少跨进程的通信次数 chunk_size = max(1, len(query_points) // (worker_count * 20)) with mp.Pool( processes=worker_count, initializer=_init_worker, initargs=(G,) ) as pool: # 用imap方法搭配tqdm实现实时进度条,分块传递任务 origins = list( tqdm( pool.imap(_get_nearest, query_points, chunksize=chunk_size), total=len(query_points), desc="查询最近路网节点" ) )
测试注意事项
- 不要在任务量低于100条的场景下测试并行性能:哪怕优化了大对象传输的开销,进程启动、本地路网初始化的固定成本依然存在,只有当任务量达到千条以上时,并行的计算收益才能覆盖固定开销。
- 如果使用osmnx 1.0及以上版本,优先调用原生批量接口
ox.distance.get_nearest_nodes,该接口支持一次性传入所有坐标点做批量近邻查询,性能远高于循环单条查询,只有当批量接口依然无法满足耗时要求时,再考虑多进程并行。
内容的提问来源于stack exchange,提问作者Antoine Van Esch
相关产品推荐
相关产品推荐

