Prange减慢了Cython循环

时间:2017-09-16 18:46:39

标签: multithreading openmp cython

考虑两种计​​算随机数的方法,一个在一个线程中,一个多线程使用带有openmp的cython prange:

def rnd_test(long size1):
    cdef long i
    for i in range(size1):
        rand()
    return 1

def rnd_test_par(long size1):
    cdef long i
    with nogil, parallel():
        for i in prange(size1, schedule='static'):
             rand()
    return 1

函数rnd_test首先使用以下setup.py

进行编译
from distutils.core import setup
from Cython.Build import cythonize

setup(
  name = 'Hello world app',
  ext_modules = cythonize("cython_test.pyx"),
)

rnd_test(100_000_000)以0.7秒运行。

然后,使用以下setup.py

编译rnd_test_par
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

ext_modules = [
    Extension(
        "cython_test_openmp",
        ["cython_test_openmp.pyx"],
        extra_compile_args=["-O3", '-fopenmp'],
        extra_link_args=['-fopenmp'],
    )

]

setup(
    name='hello-parallel-world',
    ext_modules=cythonize(ext_modules),
)

rnd_test_par(100_000_000)在10秒内运行!!!

在ipython中使用cython获得了类似的结果:

%%cython
import cython
from cython.parallel cimport parallel, prange
from libc.stdlib cimport rand

def rnd_test(long size1):
    cdef long i
    for i in range(size1):
        rand()
    return 1

%%timeit
rnd_test(100_000_000)

1次循环,最佳3:1.5秒/循环

%%cython --compile-args=-fopenmp --link-args=-fopenmp --force
import cython
from cython.parallel cimport parallel, prange
from libc.stdlib cimport rand

def rnd_test_par(long size1):
    cdef long i
    with nogil, parallel():
        for i in prange(size1, schedule='static'):
                rand()
    return 1

%%timeit
rnd_test_par(100_000_000)

1循环,最佳3:每循环8.42秒

我做错了什么?我是cython的新手,这是我第二次使用它。我上次有一个很好的经历所以我决定使用monte-carlo模拟项目(因此使用rand)。

这是预期的吗?阅读完所有文档后,我认为prange应该可以在这样一个令人尴尬的并行案例中运行良好。我不明白为什么这不能加快循环速度甚至使速度变慢。

其他一些信息:

  • 我正在运行python 3.6,cython 0.26。
  • gcc版本是“gcc(Ubuntu 5.4.0-6ubuntu1~16.04.4)5.4.0 20160609”
  • CPU使用率确认并行版本实际上使用了许多核心 (90%vs 25%的连续案例)

感谢您提供的任何帮助。我首先尝试使用numba,它确实加快了计算速度,但它还有其它问题让我想避免它。我希望Cython在这种情况下工作。

感谢!!!

2 个答案:

答案 0 :(得分:1)

通过DavidW的有用反馈和链接,我有一个用于随机数生成的多线程解决方案。 但是,单线程(矢量化)Numpy解决方案的时间节省并不是那么大。 numpy方法在1.2秒内生成1亿个数字(内存为5GB),而多线程方法则为0.7。鉴于复杂性增加(例如使用c ++库),我想知道它是否值得。也许我会留下随机数生成单线程并继续并行化此步骤之后的计算。 然而,这项练习对于理解randon数发生器的问题非常有用。最后,我想拥有可以在分布式环境中工作的框架,我现在可以看到,随着生成器基本上具有一个不可忽略的状态,随机数生成器的挑战将更大。

%%cython --compile-args=-fopenmp --link-args=-fopenmp --force
# distutils: language = c++
# distutils: extra_compile_args = -std=c++11
import cython
cimport numpy as np
import numpy as np
from cython.parallel cimport parallel, prange, threadid
cimport openmp

cdef extern from "<random>" namespace "std" nogil:
    cdef cppclass mt19937:
        mt19937() # we need to define this constructor to stack allocate classes in Cython
        mt19937(unsigned int seed) # not worrying about matching the exact int type for seed

    cdef cppclass uniform_real_distribution[T]:
        uniform_real_distribution()
        uniform_real_distribution(T a, T b)
        T operator()(mt19937 gen) # ignore the possibility of using other classes for "gen"

@cython.boundscheck(False)
@cython.wraparound(False)        
def test_rnd_par(long size):
    cdef:
        mt19937 gen
        uniform_real_distribution[double] dist = uniform_real_distribution[double](0.0,1.0)
        narr = np.empty(size, dtype=np.dtype("double"))
        double [:] narr_view = narr
        long i

    with nogil, parallel():
        gen = mt19937(openmp.omp_get_thread_num())
        for i in prange(size, schedule='static'):
            narr_view[i] = dist(gen)
    return narr

答案 1 :(得分:1)

我想指出两件事,值得您考虑:

答:如果您查看glibc中的implementation of rand(),您会发现在多线程程序中使用rand()会导致未指定的行为:生成数字总是相同的(假设我们有相同的种子),但由于可能的提升条件,你不能说哪个数字将用于哪个线程。在所有线程之间只有一个共享状态,它需要通过锁保护,否则可能会发生更糟糕的事情:

long int
__random ()
{
  int32_t retval;
  __libc_lock_lock (lock);
  (void) __random_r (&unsafe_state, &retval);
  __libc_lock_unlock (lock);
  return retval;
}

从这段代码中可以看出一个可能的解决方法,如果我们不允许使用c ++ 11:每个线程都有自己的种子,我们可以使用rand_r()方法。

此锁定是您无法看到原始版本加速的原因。

B:为什么你没有看到更快的c ++ 11解决方案?你产生5GB的数据并将其写入内存 - 这是一个非常重要的内存限制任务。因此,如果线程正在工作,则内存带宽足以传输创建的数据,而瓶颈则是下一个随机数的计算。如果有两个线程,则有两倍的数据,但没有更多的内存带宽。因此会有许多线程,内存带宽成为瓶颈,你将无法通过添加更多线程/核心来实现任何加速。

因此,并行化随机数生成没有收获?问题不是随机数生成,而是写入内存的数据量:如果创建的随机数被同一个线程消耗而不将其存储在RAM中,那么与生成数字相比,并行化将是一个更好的解决方案。一个线程并分发它们:

  1. 您不必将这些号码写入RAM。
  2. 您不必从RAM中读取这些数字。
  3. 您可以使用单个线程更快地计算它们。