Skip to main content
 首页 » 编程设计

python之为什么我的并行性能达到顶峰

2025年05月04日10三少

我最近经常使用 Python,在比较众多并行化包时,我注意到从串行到并行的性能提升似乎在 6 个进程而不是 8 个进程时达到顶峰——我的 MacBook 的核心数量Pro (OS X 10.8.2) 有。

所附图表比较了不同任务的时间作为进程数(并行或顺序)的函数。这个例子使用的是 python 内置的'multiprocessing '包“内存”与“处理器”指的是内存密集型(仅分配大型数组)与计算密集型(许多操作)功能。

8道以下出顶的原因是什么?

(“时间”是每个进程数 100 次函数调用的平均值)

import multiprocessing as mp 
import time 
import numpy as np 
import matplotlib as mpl 
from matplotlib import pyplot as plt 
 
iters       = 100 
mem_num     = 1000 
pro_num     = 20000 
max_procs   = 10 
 
line_width  = 2.0 
legend_size = 10 
fig_name    = 'timing.pdf' 
 
def UseMemory(num): 
    test1 = np.zeros([num,num]) 
    test2 = np.arange(num*num) 
    test3 = np.array(test2).reshape([num, num]) 
    test4 = np.empty(num, dtype=object) 
    return  
 
def UseProcessor(num): 
    test1 = np.arange(num) 
    test1 = np.cos(test1) 
    test1 = np.sqrt(np.fabs(test1)) 
    test2 = np.zeros(num) 
    for i in range(num): test2[i] = test1[i] 
    return np.std(test2) 
 
def MemJob(its):  
    for ii in range(its): UseMemory(mem_num) 
 
def ProJob(its):  
    for ii in range(iters): UseProcessor(pro_num) 
 
 
if __name__ == "__main__": 
 
    print '\nParTest\n'     
 
    proc_range = np.arange(1,max_procs+1,step=1) 
 
    test_times = np.zeros([len(proc_range),2,2])                 # test_times[num_procs][0-ser,1-par][0-mem,1-pro] 
    tot_times  = np.zeros([len(proc_range),2  ])                 #  tot_times[num_procs][0-ser,1-par] 
 
    print ' Testing %2d numbers of processors between [%d,%d]' % (len(proc_range), 1, max_procs) 
    print ' Iterations %d, Memory Length %d, Processor Length %d' % (iters, mem_num, pro_num) 
 
    for it in range(len(proc_range)): 
        procs = proc_range[it] 
        job_arg = procs*[iters] 
        print '\n - %2d, Processes = %3d' % (it, procs) 
 
        # --- Test Serial --- 
        print ' - - Serial' 
        # Test Memory 
        all_start = time.time() 
        start = time.time() 
        map(MemJob, [procs*iters]) 
        ser_mem_time = time.time() - start 
 
        # Test Processor 
        start = time.time() 
        map(ProJob, job_arg) 
        ser_pro_time = time.time() - start 
 
        ser_time = time.time() - all_start 
 
        # --- Test Parallel : multiprocessing --- 
        print ' - - Parallel: multiprocessing' 
        pool = mp.Pool(processes=procs) 
        # Test Memory 
        all_start = time.time() 
        start = time.time() 
        pool.map(MemJob, job_arg) 
        par_mem_time = time.time() - start 
 
        # Test Processor 
        start = time.time() 
        pool.map(ProJob, job_arg) 
        par_pro_time = time.time() - start 
 
        par_time = time.time() - all_start 
 
        print ' - - Collecting' 
        ser_mem_time /= procs 
        ser_pro_time /= procs 
        par_mem_time /= procs 
        par_pro_time /= procs 
        ser_time     /= procs 
        par_time     /= procs 
 
        test_times[it][0] = [ ser_mem_time, ser_pro_time ] 
        test_times[it][1] = [ par_mem_time, par_pro_time ] 
        tot_times[it]     = [ ser_time    , par_time     ] 
 
 
 
    fig = plt.figure() 
    ax  = fig.add_subplot(111) 
    ax.set_xlabel('Number of Processes') 
    ax.set_ylabel('Time [s]') 
    ax.xaxis.grid(True) 
    ax.yaxis.grid(True) 
    lines = [] 
    names = [] 
 
    l1, = ax.plot(proc_range, test_times[:,0,0], linewidth=line_width) 
    lines.append(l1) 
    names.append('Serial Memory') 
    l1, = ax.plot(proc_range, test_times[:,0,1], linewidth=line_width) 
    lines.append(l1) 
    names.append('Serial Processor') 
    l1, = ax.plot(proc_range, tot_times[:,0], linewidth=line_width) 
    lines.append(l1) 
    names.append('Serial') 
 
    l1, = ax.plot(proc_range, test_times[:,1,0], linewidth=line_width) 
    lines.append(l1) 
    names.append('Parallel Memory') 
    l1, = ax.plot(proc_range, test_times[:,1,1], linewidth=line_width) 
    lines.append(l1) 
    names.append('Parallel Processor') 
    l1, = ax.plot(proc_range, tot_times[:,1], linewidth=line_width) 
    lines.append(l1) 
    names.append('Parallel') 
 
    plt.legend(lines, names, ncol=2, prop={'size':legend_size}, fancybox=True, shadow=True, bbox_to_anchor=(1.10, 1.10)) 
    fig.savefig(fig_name,dpi=fig.get_dpi()) 
    print ' - Saved to ', fig_name 
    plt.show(block=True) 

请您参考如下方法:

从上面的讨论中,我认为您已经获得了所需的信息,但我添加了一个答案来收集事实,以防其他人受益(而且我想自己解决)。 (感谢@bamboon,他首先提到了其中的一些内容。)

首先,您的 MacBook 有一个带有四个物理内核的 CPU,但芯片的设计使得每个内核的硬件都能够运行两个线程。这称为“同步多线程”(SMT),在这种情况下体现为英特尔的 hyperthreading特征。所以总的来说你有 8 个“虚拟核心”(4 + 4 = 8)。

请注意,操作系统对所有虚拟内核一视同仁,即它不区分物理内核提供的两个 SMT 线程,这就是为什么 sysctl 在您查询时返回 8 的原因。 Python 会做同样的事情:

>>> import multiprocessing 
>>> multiprocessing.cpu_count() 
8 

其次,您遇到的加速限制是一种众所周知的现象,在这种情况下,并行性能会饱和,并且不会随着处理该问题的更多处理器的增加而提高。此效果由 Amdahl's Law 描述,关于根据可以并行化的代码量和串行运行的代码量,期望从多个处理器获得多少加速的定量陈述。

通常有许多因素会限制相对加速,包括操作系统的细节甚至计算机的体系结构(例如 SMT 如何在硬件核心中工作),因此即使您尽可能多地并行化代码,您的性能不会无限扩大。了解串行瓶颈在哪里可能需要对您的程序及其运行环境进行非常详细的分析。

您可以在 this question 中找到一个很好的讨论示例.

希望对您有所帮助。