Cython 对于 4 个线程比范围更慢
Cython prange slower for 4 threads then with range
我目前正在尝试按照一个简单的示例使用 cython 的 prange 并行化循环。
我已经安装了允许 openmp 的 OpenBlas 0.2.14,并从针对 openblas 的源代码编译了 numpy 1.10.1 和 scipy 0.16。为了测试库的性能,我遵循以下示例:http://nealhughes.net/parallelcomp2/。
要定时的功能从站点复制:
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
当我在我的机器上 运行 这些示例时(macbook pro 与 osx 10.10),我得到以下导出时间 OMP_NUM_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_NUM_THREADS=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 机器上看到了同样的行为,因此我的问题。作者如何在我的 2 个系统上的 4 个线程的相同代码 运行 速度变慢 4 倍的情况下获得 4 倍的速度。
生成 *.c & .so
的设置脚本也与博客中使用的相同。
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
的一个重要特征(与任何其他 parallel 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
这些结果是 num_threads=4
(我有 4 个逻辑核心),加速大约是 2 倍。在更进一步之前,我们仍然可以通过声明我们的数组是连续的,即用 double[::1]
.
声明 X
和 Y
来进一步削减一些 ms
连续数组:
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) 工作 scheduling 更重要,这就是您的基准所遭受的痛苦。默认情况下,块大小是在编译时确定的,即 schedule=static
然而,环境变量(例如 OMP_SCHEDULE)和两台机器(你的和博客中的机器)的工作负载很可能 post) 是不同的,它们在运行时、动态地、引导式等方式调度作业。让我们通过将 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
条件,即只保留 Y[i] = c_exp(X[i])
在 for 循环中。这是因为 if-else
语句也会对并行代码中的分支预测和乱序执行产生不利影响。在我的机器上,通过此更改,我的串行代码速度提高了近 2.7 倍。
我目前正在尝试按照一个简单的示例使用 cython 的 prange 并行化循环。 我已经安装了允许 openmp 的 OpenBlas 0.2.14,并从针对 openblas 的源代码编译了 numpy 1.10.1 和 scipy 0.16。为了测试库的性能,我遵循以下示例:http://nealhughes.net/parallelcomp2/。 要定时的功能从站点复制:
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
当我在我的机器上 运行 这些示例时(macbook pro 与 osx 10.10),我得到以下导出时间 OMP_NUM_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_NUM_THREADS=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 机器上看到了同样的行为,因此我的问题。作者如何在我的 2 个系统上的 4 个线程的相同代码 运行 速度变慢 4 倍的情况下获得 4 倍的速度。
生成 *.c & .so
的设置脚本也与博客中使用的相同。
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
的一个重要特征(与任何其他 parallel 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
这些结果是 num_threads=4
(我有 4 个逻辑核心),加速大约是 2 倍。在更进一步之前,我们仍然可以通过声明我们的数组是连续的,即用 double[::1]
.
X
和 Y
来进一步削减一些 ms
连续数组:
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) 工作 scheduling 更重要,这就是您的基准所遭受的痛苦。默认情况下,块大小是在编译时确定的,即 schedule=static
然而,环境变量(例如 OMP_SCHEDULE)和两台机器(你的和博客中的机器)的工作负载很可能 post) 是不同的,它们在运行时、动态地、引导式等方式调度作业。让我们通过将 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
条件,即只保留 Y[i] = c_exp(X[i])
在 for 循环中。这是因为 if-else
语句也会对并行代码中的分支预测和乱序执行产生不利影响。在我的机器上,通过此更改,我的串行代码速度提高了近 2.7 倍。