Python Cython prange在4个线程中速度较慢,然后在范围内

Python Cython prange在4个线程中速度较慢,然后在范围内,python,numpy,cython,openblas,Python,Numpy,Cython,Openblas,我目前正试图遵循一个简单的例子,用cython的prange并行化一个循环。 我安装了OpenBlas 0.2.14,允许使用openmp,并针对OpenBlas从源代码处编译了numpy 1.10.1和scipy 0.16。要测试库的性能,我将遵循以下示例:。 要计时的功能从站点复制: import numpy as np from math import exp from libc.math cimport exp as c_exp from cython.parallel import

我目前正试图遵循一个简单的例子,用cython的prange并行化一个循环。 我安装了OpenBlas 0.2.14,允许使用openmp,并针对OpenBlas从源代码处编译了numpy 1.10.1和scipy 0.16。要测试库的性能,我将遵循以下示例:。 要计时的功能从站点复制:

import numpy as np
from math import exp 
from libc.math cimport exp as c_exp
from cython.parallel import prange,parallel

def array_f(X):

    Y = np.zeros(X.shape)
    index = X > 0.5
    Y[index] = np.exp(X[index])

    return Y

def c_array_f(double[:] X):

    cdef int N = X.shape[0]
    cdef double[:] Y = np.zeros(N)
    cdef int i

    for i in range(N):
        if X[i] > 0.5:
            Y[i] = c_exp(X[i])
        else:
            Y[i] = 0

    return Y


def c_array_f_multi(double[:] X):

    cdef int N = X.shape[0]
    cdef double[:] Y = np.zeros(N)
    cdef int i
    with nogil, parallel():
        for i in prange(N):
            if X[i] > 0.5:
                Y[i] = c_exp(X[i])
            else:
                Y[i] = 0

    return Y
代码作者报告了4个内核的以下加速:

from thread_demo import *
import numpy as np
X = -1 + 2*np.random.rand(10000000) 
%timeit array_f(X)
1 loops, best of 3: 222 ms per loop
%timeit c_array_f(X)
10 loops, best of 3: 87.5 ms per loop 
%timeit c_array_f_multi(X)
10 loops, best of 3: 22.4 ms per loop
当我在我的机器上运行这些示例时(带有osx 10.10的macbook pro),我得到了导出
OMP\u NUM\u THREADS=1

In [1]: from bla import *
In [2]: import numpy as np
In [3]: X = -1 + 2*np.random.rand(10000000)
In [4]: %timeit c_array_f(X)
10 loops, best of 3: 89.7 ms per loop
In [5]: %timeit c_array_f_multi(X)
1 loops, best of 3: 343 ms per loop
对于
OMP\u NUM\u线程=4

In [1]: from bla import *
In [2]: import numpy as np
In [3]: X = -1 + 2*np.random.rand(10000000)
In [4]: %timeit c_array_f(X)
10 loops, best of 3: 89.5 ms per loop
In [5]: %timeit c_array_f_multi(X)
10 loops, best of 3: 119 ms per loop
我在openSuse机器上看到了同样的行为,因此我提出了这个问题。在我的两个系统上,当相同的代码在4个线程中运行得较慢时,作者如何能将速度提高4倍

用于生成
*.c&.so
的设置脚本也与blog中使用的脚本相同

from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension
from Cython.Distutils import build_ext
import numpy as np

ext_modules=[
    Extension("bla",
              ["bla.pyx"],
              libraries=["m"],
              extra_compile_args = ["-O3", "-ffast-math","-march=native", "-fopenmp" ],
              extra_link_args=['-fopenmp'],
              include_dirs = [np.get_include()]
              ) 
]

setup( 
  name = "bla",
  cmdclass = {"build_ext": build_ext},
  ext_modules = ext_modules
)
如果有人能向我解释为什么会发生这种情况,那就太好了。

1)prange
的一个重要特性(与任何其他
并行for
循环一样)是它激活无序执行,这意味着循环可以以任意顺序执行。当迭代之间没有数据依赖性时,无序执行确实会有回报

我不知道Cython的内部结构,但我认为如果没有关闭
boundscheck
ing,循环就不能任意执行,因为下一次迭代将取决于数组在当前迭代中是否超出范围,因此问题几乎成了串行的,因为线程将不得不等待结果。这是代码的问题之一。事实上,Cython确实给了我以下警告:

warning: bla.pyx:42:16: Use boundscheck(False) for faster access
因此,添加以下内容

from cython import boundscheck, wraparound

@boundscheck(False)
@wraparound(False)
def c_array_f(double[:] X):
   # Rest of your code

@boundscheck(False)
@wraparound(False)
def c_array_f_multi(double[:] X):
   # Rest of your code
现在,让我们用数据对它们计时
X=-1+2*np.random.rand(10000000)

带边界检查:

In [2]:%timeit array_f(X)
10 loops, best of 3: 189 ms per loop
In [4]:%timeit c_array_f(X)
10 loops, best of 3: 93.6 ms per loop
In [5]:%timeit c_array_f_multi(X)
10 loops, best of 3: 103 ms per loop
In [9]:%timeit c_array_f(X)
10 loops, best of 3: 84.2 ms per loop
In [10]:%timeit c_array_f_multi(X)
10 loops, best of 3: 42.3 ms per loop
In [14]:%timeit c_array_f(X)
10 loops, best of 3: 81.8 ms per loop
In [15]:%timeit c_array_f_multi(X)
10 loops, best of 3: 39.3 ms per loop
无边界检查:

In [2]:%timeit array_f(X)
10 loops, best of 3: 189 ms per loop
In [4]:%timeit c_array_f(X)
10 loops, best of 3: 93.6 ms per loop
In [5]:%timeit c_array_f_multi(X)
10 loops, best of 3: 103 ms per loop
In [9]:%timeit c_array_f(X)
10 loops, best of 3: 84.2 ms per loop
In [10]:%timeit c_array_f_multi(X)
10 loops, best of 3: 42.3 ms per loop
In [14]:%timeit c_array_f(X)
10 loops, best of 3: 81.8 ms per loop
In [15]:%timeit c_array_f_multi(X)
10 loops, best of 3: 39.3 ms per loop
这些结果是使用
num_threads=4
(我有4个逻辑核)得到的,速度大约是2倍。在进一步讨论之前,我们仍然可以通过声明数组是连续的,即使用
double[::1]
声明
X
Y
来减少一些
ms

连续数组:

In [2]:%timeit array_f(X)
10 loops, best of 3: 189 ms per loop
In [4]:%timeit c_array_f(X)
10 loops, best of 3: 93.6 ms per loop
In [5]:%timeit c_array_f_multi(X)
10 loops, best of 3: 103 ms per loop
In [9]:%timeit c_array_f(X)
10 loops, best of 3: 84.2 ms per loop
In [10]:%timeit c_array_f_multi(X)
10 loops, best of 3: 42.3 ms per loop
In [14]:%timeit c_array_f(X)
10 loops, best of 3: 81.8 ms per loop
In [15]:%timeit c_array_f_multi(X)
10 loops, best of 3: 39.3 ms per loop
2)更重要的是工作,而这正是你的基准所遭受的痛苦。默认情况下,块大小是在编译时确定的,即
schedule=static
,但是环境变量(例如OMP_schedule)和两台机器(您的机器和博客文章中的机器)的工作负载很可能是不同的,它们在运行时、动态地、有指导地安排作业,等等。让我们将您的
prange
替换为

for i in prange(N, schedule='static'):
    # static scheduling... 
for i in prange(N, schedule='dynamic'):
    # dynamic scheduling... 
现在让我们对它们计时(仅限多线程代码):

调度效果:

In [23]:%timeit c_array_f_multi(X) # static
10 loops, best of 3: 39.5 ms per loop
In [28]:%timeit c_array_f_multi(X) # dynamic
1 loops, best of 3: 319 ms per loop

根据您自己机器上的工作负载,您可能能够复制这一点。作为旁注,由于您只是尝试在微基准测试中测量并行与串行代码的性能,而不是实际代码,因此我建议您消除
if-else
条件,即仅在for循环中保持
Y[I]=c_exp(X[I])
。这是因为
if-else
语句也会对并行代码中的分支预测和无序执行产生不利影响。在我的机器上,通过这一改变,我的串行代码速度提高了2.7倍

谢谢你的提示。我已经更新了帖子作为回应。