python - Python 中的多处理 : Numpy + Vector Summation -> Huge Slowdown

标签 python performance parallel-processing multiprocessing slowdown

请不要因为长篇大论而气馁。我尽量提供尽可能多的数据,我真的需要帮助解决这个问题:S。如果有新的提示或想法,我会每天更新

问题:

我尝试在并行进程的帮助下在两核机器上并行运行 Python 代码(以避免 GIL),但存在代码显着减慢的问题。例如,在单核机器上运行每个工作负载需要 600 秒,但在双核机器上运行需要 1600 秒(每个工作负载 800 秒)。

我已经尝试过的:

  • 我测量了内存,似乎没有内存问题。
    [仅在最高点使用 20%]。
  • 我使用“htop”来检查我是否真的在不同的内核上运行程序,或者我的内核关联是否搞砸了。但也不走运,我的程序正在我所有的内核上运行。
  • 问题是 CPU 受限的问题,因此我检查并确认我的代码大部分时间都在所有内核上以 100% CPU 运行。
  • 我检查了进程 ID,确实产生了两个不同的进程。
  • 我将我提交给执行程序的函数 [ e.submit(function,[…]) ] 更改为计算饼函数,并观察到了巨大的加速。所以问题很可能出现在我提交给执行程序的 process_function(...) 中,而不是之前的代码中。
  • 目前我正在使用“并发”中的“ future ”来并行化任务。但我也尝试了“多处理”中的“池”类。然而,结果还是一样。

  • 代码:
  • 生成过程:
    result = [None]*psutil.cpu_count()
    
    e = futures.ProcessPoolExecutor( max_workers=psutil.cpu_count() )
    
    for i in range(psutil.cpu_count()):
        result[i] = e.submit(process_function, ...)
    
  • process_function:
    from math import floor
    from math import ceil
    import numpy
    import MySQLdb
    import time
    
    db = MySQLdb.connect(...)
    cursor  = db.cursor()
    query = "SELECT ...."
    cursor.execute(query)
    
    [...]  #save db results into the variable db_matrix (30 columns, 5.000 rows)
    [...]  #save db results into the variable bp_vector (3 columns, 500 rows)
    [...]  #save db results into the variable option_vector( 3 columns, 4000 rows)
    
    cursor.close()
    db.close()
    
    counter = 0 
    
    for i in range(4000):
        for j in range(500):
             helper[:] = (1-bp_vector[j,0]-bp_vector[j,1]-bp_vector[j,2])*db_matrix[:,0] 
                         + db_matrix[:,option_vector[i,0]] * bp_vector[j,0]  
                         + db_matrix[:,option_vector[i,1]] * bp_vector[j,1]   
                         + db_matrix[:,option_vector[i,2]] * bp_vector[j,2]
    
             result[counter,0] = (helper < -7.55).sum()
    
             counter = counter + 1
    
    return result
    

  • 我猜:
  • 我的猜测是,由于某种原因,创建向量“助手”的加权向量乘法导致了问题。 [我相信时间测量部分证实了这个猜测]
  • 是不是这样,numpy 会造成这些问题? numpy 与多处理兼容吗?如果没有,我该怎么办? 【已在评论中回答】
  • 会不会是因为缓存内存的原因?我在论坛上读到它,但说实话,并没有真正理解它。但如果问题出在那里,我会让自己熟悉这个话题。

  • 时间测量:(编辑)
  • 一个核心:从数据库获取数据的时间:8 秒。
  • 两个核心:从数据库获取数据的时间:12 秒。
  • 一个核心:在 process_function 中进行双循环的时间:~ 640 秒。
  • 二核:在process_function中做双循环的时间:~1600秒

  • 更新:(编辑)

    当我为循环中的每 100 个 i 测量两个进程的时间时,我发现当我仅在一个进程上运行时测量相同的事物时,我观察到的时间大约为 220%。但更神秘的是,如果我在运行过程中退出一个进程,另一个进程会加速!然后另一个过程实际上加速到与单人运行期间相同的水平。所以,我现在看不到的进程之间肯定有一些依赖关系:S

    更新 2:(编辑)

    因此,我又进行了几次测试运行和测量。在测试运行中,我使用了 作为计算实例。单核linux机器 (n1-standard-1, 1 vCPU, 3.75 GB 内存) 或 二核linux机 (n1-standard-2、2 个 vCPU、7.5 GB 内存)来自 Google 云计算引擎。但是,我也在本地计算机上进行了测试,并观察到大致相同的结果。 (-> 因此,虚拟化环境应该没问题)。结果如下:

    P.S:这里的时间与上面的测量不同,因为我对循环进行了一些限制,并在 Google Cloud 而不是我的家用电脑上进行了测试。

    1-core machine, started 1 process:

    time: 225sec , CPU utilization: ~100%

    1-core machine, started 2 process:

    time: 557sec , CPU utilization: ~100%

    1-core machine, started 1 process, limited max. CPU-utilization to 50%:

    time: 488sec , CPU utilization: ~50%



    .

    2-core machine, started 2 process:

    time: 665sec , CPU-1 utilization: ~100% , CPU-2 utilization: ~100%

    the process did not jumped between the cores, each used 1 core

    (at least htop displayed these results with the “Process” column)

    2-core machine, started 1 process:

    time: 222sec , CPU-1 utilization: ~100% (0%) , CPU-2 utilization: ~0% (100%)

    however, the process jumped sometimes between the cores

    2-core machine, started 1 process, limited max. CPU-utilization to 50%:

    time: 493sec , CPU-1 utilization: ~50% (0%) , CPU-2 utilization: ~0% (100%)

    however, the process jumped extremely often between the cores



    我使用“htop”和python模块“time”来获得这些结果。

    更新 - 3:(编辑)

    我使用 cProfile 来分析我的代码:
    python -m cProfile -s cumtime fun_name.py
    

    这些文件太长,无法在此处发布,但我相信,如果它们包含有值(value)的信息,则此信息可能位于结果文本之上。因此,我将在这里发布结果的第一行:

    1核机器,启动1个进程:
    623158 function calls (622735 primitive calls) in 229.286 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall   filename:lineno(function)
            1    0.371    0.371  229.287  229.287   20_with_multiprocessing.py:1(<module>)
            3    0.000    0.000  225.082   75.027   threading.py:309(wait)
            1    0.000    0.000  225.082  225.082   _base.py:378(result)
           25  225.082    9.003  225.082    9.003   {method 'acquire' of 'thread.lock' objects}
            1    0.598    0.598    3.081    3.081   get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.877    0.959   cursors.py:164(execute)
            3    0.000    0.000    2.877    0.959   cursors.py:353(_query)
            3    0.000    0.000    1.958    0.653   cursors.py:315(_do_query)
            3    0.000    0.000    1.943    0.648   cursors.py:142(_do_get_result)
            3    0.000    0.000    1.943    0.648   cursors.py:351(_get_result)
            3    1.943    0.648    1.943    0.648   {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    0.919    0.306   cursors.py:358(_post_get_result)
            3    0.000    0.000    0.917    0.306   cursors.py:324(_fetch_row)
            3    0.917    0.306    0.917    0.306   {built-in method fetch_row}
       591314    0.161    0.000    0.161    0.000   {range}
    

    1核机器,启动2个进程:
    626052 function calls (625616 primitive calls) in 578.086 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall   filename:lineno(function)
            1    0.310    0.310  578.087  578.087   20_with_multiprocessing.py:1(<module>)
           30  574.310   19.144  574.310   19.144   {method 'acquire' of 'thread.lock' objects}
            2    0.000    0.000  574.310  287.155   _base.py:378(result)
            3    0.000    0.000  574.310  191.437   threading.py:309(wait)
            1    0.544    0.544    2.854    2.854   get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.563    0.854   cursors.py:164(execute)
            3    0.000    0.000    2.563    0.854   cursors.py:353(_query)
            3    0.000    0.000    1.715    0.572   cursors.py:315(_do_query)
            3    0.000    0.000    1.701    0.567   cursors.py:142(_do_get_result)
            3    0.000    0.000    1.701    0.567   cursors.py:351(_get_result)
            3    1.701    0.567    1.701    0.567   {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    0.848    0.283   cursors.py:358(_post_get_result)
            3    0.000    0.000    0.847    0.282   cursors.py:324(_fetch_row)
            3    0.847    0.282    0.847    0.282   {built-in method fetch_row}
       591343    0.152    0.000    0.152    0.000   {range}
    

    .

    2核机器,启动1个进程:
    623164 function calls (622741 primitive calls) in 235.954 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall   filename:lineno(function)
            1    0.246    0.246  235.955  235.955   20_with_multiprocessing.py:1(<module>)
            3    0.000    0.000  232.003   77.334   threading.py:309(wait)
           25  232.003    9.280  232.003    9.280   {method 'acquire' of 'thread.lock' objects}
            1    0.000    0.000  232.003  232.003   _base.py:378(result)
            1    0.593    0.593    3.104    3.104   get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.774    0.925   cursors.py:164(execute)
            3    0.000    0.000    2.774    0.925   cursors.py:353(_query)
            3    0.000    0.000    1.981    0.660   cursors.py:315(_do_query)
            3    0.000    0.000    1.970    0.657   cursors.py:142(_do_get_result)
            3    0.000    0.000    1.969    0.656   cursors.py:351(_get_result)
            3    1.969    0.656    1.969    0.656   {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    0.794    0.265 cursors.py:358(_post_get_result)  
            3    0.000    0.000    0.792    0.264   cursors.py:324(_fetch_row)
            3    0.792    0.264    0.792    0.264   {built-in method fetch_row}
       591314    0.144    0.000    0.144    0.000   {range}
    

    2核机器,启动2个进程:
    626072 function calls (625636 primitive calls) in 682.460 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall   filename:lineno(function)
            1    0.334    0.334  682.461  682.461   20_with_multiprocessing.py:1(<module>)
            4    0.000    0.000  678.231  169.558   threading.py:309(wait)
           33  678.230   20.552  678.230   20.552   {method 'acquire' of 'thread.lock' objects}
            2    0.000    0.000  678.230  339.115   _base.py:378(result)
            1    0.527    0.527    2.974    2.974   get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.723    0.908   cursors.py:164(execute)
            3    0.000    0.000    2.723    0.908   cursors.py:353(_query)
            3    0.000    0.000    1.749    0.583   cursors.py:315(_do_query)
            3    0.000    0.000    1.736    0.579   cursors.py:142(_do_get_result)
            3    0.000    0.000    1.736    0.579   cursors.py:351(_get_result)
            3    1.736    0.579    1.736    0.579   {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    0.975    0.325   cursors.py:358(_post_get_result)
            3    0.000    0.000    0.973    0.324   cursors.py:324(_fetch_row)
            3    0.973    0.324    0.973    0.324   {built-in method fetch_row}
            5    0.093    0.019    0.304    0.061   __init__.py:1(<module>)
            1    0.017    0.017    0.275    0.275   __init__.py:106(<module>)
            1    0.005    0.005    0.198    0.198   add_newdocs.py:10(<module>)
       591343    0.148    0.000    0.148    0.000   {range}
    

    我个人真的不知道如何处理这些结果。很高兴收到提示、提示或任何其他帮助 - 谢谢:)

    回复 Answer-1:(编辑)

    Roland Smith 查看了数据并建议,多处理对性能的影响可能大于其帮助。因此,我在没有多处理的情况下又进行了一次测量(如他建议的代码):

    我的结论是否正确,事实并非如此?因为测量的时间似乎与多处理之前测量的时间相似?

    1核机:

    Database access took 2.53 seconds

    Matrix manipulation took 236.71 seconds


    1842384 function calls (1841974 primitive calls) in 241.114 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall   filename:lineno(function)
            1  219.036  219.036  241.115  241.115   20_with_multiprocessing.py:1(<module>)
       406000    0.873    0.000   18.097    0.000   {method 'sum' of 'numpy.ndarray' objects}
       406000    0.502    0.000   17.224    0.000   _methods.py:31(_sum)
       406001   16.722    0.000   16.722    0.000   {method 'reduce' of 'numpy.ufunc' objects}
            1    0.587    0.587    3.222    3.222   get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.964    0.988   cursors.py:164(execute)
            3    0.000    0.000    2.964    0.988   cursors.py:353(_query)
            3    0.000    0.000    1.958    0.653   cursors.py:315(_do_query)
            3    0.000    0.000    1.944    0.648   cursors.py:142(_do_get_result)
            3    0.000    0.000    1.944    0.648   cursors.py:351(_get_result)
            3    1.944    0.648    1.944    0.648   {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    1.006    0.335   cursors.py:358(_post_get_result)
            3    0.000    0.000    1.005    0.335   cursors.py:324(_fetch_row)
            3    1.005    0.335    1.005    0.335   {built-in method fetch_row}
       591285    0.158    0.000    0.158    0.000   {range}
    

    2核机:

    Database access took 2.32 seconds

    Matrix manipulation took 242.45 seconds


    1842390 function calls (1841980 primitive calls) in 246.535 seconds
    
       Ordered by: cumulative time
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1  224.705  224.705  246.536  246.536 20_with_multiprocessing.py:1(<module>)
       406000    0.911    0.000   17.971    0.000 {method 'sum' of 'numpy.ndarray' objects}
       406000    0.526    0.000   17.060    0.000 _methods.py:31(_sum)
       406001   16.534    0.000   16.534    0.000 {method 'reduce' of 'numpy.ufunc' objects}
            1    0.617    0.617    3.113    3.113 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
            3    0.000    0.000    2.789    0.930 cursors.py:164(execute)
            3    0.000    0.000    2.789    0.930 cursors.py:353(_query)
            3    0.000    0.000    1.938    0.646 cursors.py:315(_do_query)
            3    0.000    0.000    1.920    0.640 cursors.py:142(_do_get_result)
            3    0.000    0.000    1.920    0.640 cursors.py:351(_get_result)
            3    1.920    0.640    1.920    0.640 {method 'store_result' of '_mysql.connection' objects}
            3    0.001    0.000    0.851    0.284 cursors.py:358(_post_get_result)
            3    0.000    0.000    0.849    0.283 cursors.py:324(_fetch_row)
            3    0.849    0.283    0.849    0.283 {built-in method fetch_row}
       591285    0.160    0.000    0.160    0.000 {range}
    

    最佳答案

    您的程序似乎大部分时间都花在获取锁上。这似乎表明,在您的情况下,多处理弊大于利。

    删除所有多处理的东西并开始测量没有它需要多长时间。例如。像这样。

    from math import floor
    from math import ceil
    import numpy
    import MySQLdb
    import time
    
    start = time.clock()
    db = MySQLdb.connect(...)
    cursor  = db.cursor()
    query = "SELECT ...."
    cursor.execute(query)
    stop = time.clock()
    print "Database access took {:.2f} seconds".format(stop - start)
    
    start = time.clock()
    [...]  #save db results into the variable db_matrix (30 columns, 5.000 rows)
    [...]  #save db results into the variable bp_vector (3 columns, 500 rows)
    [...]  #save db results into the variable option_vector( 3 columns, 4000 rows)
    stop = time.clock()
    print "Creating matrices took {:.2f} seconds".format(stop - start)
    cursor.close()
    db.close()
    
    counter = 0 
    
    start = time.clock()
    for i in range(4000):
        for j in range(500):
             helper[:] = (1-bp_vector[j,0]-bp_vector[j,1]-bp_vector[j,2])*db_matrix[:,0] 
                         + db_matrix[:,option_vector[i,0]] * bp_vector[j,0]  
                         + db_matrix[:,option_vector[i,1]] * bp_vector[j,1]   
                         + db_matrix[:,option_vector[i,2]] * bp_vector[j,2]
    
             result[counter,0] = (helper < -7.55).sum()
    
             counter = counter + 1
    stop = time.clock()
    print "Matrix manipulation took {:.2f} seconds".format(stop - start)
    

    编辑-1

    根据你的测量,我坚持我的结论(稍微改写),在多核机器上,使用 multiprocessing正如你现在所做的那样,对你的表现非常不利。在双核机器上,具有多处理功能的程序比没有它的程序花费的时间要长得多!

    我认为,在使用单核机器时使用多处理与不使用多处理之间没有区别并不是很重要。无论如何,单核机器不会从多处理中看到太多好处。

    新的测量结果表明,大部分时间都花在了矩阵操作上。这是合乎逻辑的,因为您使用的是显式嵌套的 for 循环,这不是很快。

    基本上有四种可能的解决方案;

    第一种是将嵌套循环重写为 numpy 操作。 Numpy 操作具有隐式循环(用 C 编写)而不是 Python 中的显式循环,因此速度更快。 (显式比隐式更糟糕的罕见情况。;-) )缺点是这可能会使用大量内存。

    第二种选择是拆分 helper 的计算, 由 4 个部分组成。在单独的过程中执行每个部分,并在最后将结果相加。这确实会产生一些开销;每个进程都必须从数据库中检索所有数据,并且必须将部分结果传输回主进程(也可能通过数据库?)。

    第三个选项可能是使用 pypy而不是 Cpython .它可以明显更快。

    第四种选择是用 Cython 或 C 重写关键矩阵操作。

    关于python - Python 中的多处理 : Numpy + Vector Summation -> Huge Slowdown,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36383962/

    相关文章:

    python - 如何错误检查 Python 统计模式功能?

    python - 使用 pip 安装单文件 python 模块

    python - Django super 函数不向类添加变量

    c# - 如果将字符串引用设置为引号中的字符串,编译器是否会创建额外的字符串?

    haskell - 诊断并行 monad 性能

    c++ - 有人可以帮我并行化这个 C++ 代码吗?

    python - 在 Python 中识别列表中的重复值

    python - 如何有效地从多个列表中获取一组唯一值 (Python)

    MongoDB oplog 包含许多 Noops

    parallel-processing - 如何与 Julia CUDArt 同步?