考虑两种计算随机数的方法,一个在一个线程中,一个多线程使用带有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_parfrom 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应该可以在这样一个令人尴尬的并行案例中运行良好。我不明白为什么这不能加快循环速度甚至使速度变慢。
其他一些信息:
感谢您提供的任何帮助。我首先尝试使用numba,它确实加快了计算速度,但它还有其它问题让我想避免它。我希望Cython在这种情况下工作。
感谢!!!
答案 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中,那么与生成数字相比,并行化将是一个更好的解决方案。一个线程并分发它们: