我正在使用R创建一个模拟,该模拟从随机正态分布中抽取样本,这并不奇怪,它相当慢。因此,我寻找了一些使用Rcpp加快速度的方法,并遇到了RcppZiggurat package用于更快的随机普通样本,以及RcppParallel package用于多线程计算,并且想一想为什么不同时使用更快的算法并并行绘制样本?
因此,我开始制作原型,最后使用三种方法进行比较:
rnorm
以下是我使用RcppParallel + RcppZiggurat(parallelDraws
函数)和仅RcppZiggurat(serialDraws
函数)的实现:
#include <Rcpp.h>
// [[Rcpp::plugins("cpp11")]]
// [[Rcpp::depends(RcppParallel)]]
#include <RcppParallel.h>
// [[Rcpp::depends(RcppZiggurat)]]
#include <Ziggurat.h>
static Ziggurat::Ziggurat::Ziggurat zigg;
using namespace RcppParallel;
struct Norm : public Worker
{
int input;
// saved draws
RVector<double> draws;
// constructors
Norm(const int input, Rcpp::NumericVector draws)
: input(input), draws(draws) {}
void operator()(std::size_t begin, std::size_t end) {
for (std::size_t i = begin; i < end; i++) {
draws[i] = zigg.norm();
}
}
};
// [[Rcpp::export]]
Rcpp::NumericVector parallelDraws(int x) {
// allocate the output vector
Rcpp::NumericVector draws(x);
// declare the Norm instance
Norm norm(x, draws);
// call parallelFor to start the work
parallelFor(0, x, norm);
// return the draws
return draws;
};
// [[Rcpp::export]]
Rcpp::NumericVector serialDraws(int x) {
// allocate the output vector
Rcpp::NumericVector draws(x);
for (int i = 0; i < x; i++) {
draws[i] = zigg.norm();
}
// return the draws
return draws;
};
当我对它们进行基准测试时,我发现了一些令人惊讶的结果:
library(microbenchmark)
microbenchmark(parallelDraws(1e5), serialDraws(1e5), rnorm(1e5))
Unit: microseconds
expr min lq mean median uq max neval
parallelDraws(1e+05) 3113.752 3539.686 3687.794 3599.1540 3943.282 5058.376 100
serialDraws(1e+05) 695.501 734.593 2536.940 757.2325 806.135 175712.496 100
rnorm(1e+05) 6072.043 6264.030 6655.835 6424.0195 6661.739 18578.669 100
单独使用RcppZiggurat比rnorm
快8倍,但是一起使用RcppParallel和RcppZiggurat比单独使用RcppZiggurat慢!我尝试使用RcppParallel ParallelFor
函数的grain size,但是并没有带来任何明显的改进。
我的问题是:添加并行性实际上在这里变得更糟的原因可能是什么?我知道,根据各种因素,并行计算中的“开销”可能会超过收益。那是这里发生的事吗?还是我完全误解了如何有效地使用RcppParallel软件包?
答案 0 :(得分:2)
如评论中所述,开销可能会成问题,尤其是在整体运行时间较短时,最好不要将输出向量初始化为零,而应使用线程本地RNG。实施示例:
#include <Rcpp.h>
// [[Rcpp::plugins("cpp11")]]
// [[Rcpp::depends(RcppParallel)]]
#include <RcppParallel.h>
// [[Rcpp::depends(RcppZiggurat)]]
#include <Ziggurat.h>
using namespace RcppParallel;
struct Norm : public Worker
{
// saved draws
RVector<double> draws;
// constructors
Norm(Rcpp::NumericVector draws)
: draws(draws) {}
void operator()(std::size_t begin, std::size_t end) {
Ziggurat::Ziggurat::Ziggurat zigg(end);
for (std::size_t i = begin; i < end; i++) {
draws[i] = zigg.norm();
}
}
};
// [[Rcpp::export]]
Rcpp::NumericVector parallelDraws(int x) {
// allocate the output vector
Rcpp::NumericVector draws(Rcpp::no_init(x));
Norm norm(draws);
parallelFor(0, x, norm);
return draws;
}
// [[Rcpp::export]]
Rcpp::NumericVector serialDraws(int x) {
// allocate the output vector
Rcpp::NumericVector draws(Rcpp::no_init(x));
Ziggurat::Ziggurat::Ziggurat zigg(42);
for (int i = 0; i < x; i++) {
draws[i] = zigg.norm();
}
return draws;
}
请注意,我使用的是“穷人的并行RNG”,即不同线程的不同种子,并希望达到最好。我使用end
作为种子,因为begin
可能为零,而且我不确定RcppZiggurat中的RNG是否喜欢那样。由于创建一个Ziggurat
对象需要花费一些时间(和内存),因此我也使用本地对象进行公平的串行计算。
对于10 ^ 5次随机抽奖,使用并行计算仍然没有好处:
> bench::mark(parallelDraws(1e5), serialDraws(1e5), check = FALSE, min_iterations = 10)[,1:5]
# A tibble: 2 x 5
expression min median `itr/sec` mem_alloc
<bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt>
1 parallelDraws(1e+05) 1.08ms 1.78ms 558. 784KB
2 serialDraws(1e+05) 624.16µs 758.6µs 1315. 784KB
但是在10 ^ 8抽奖时,我的双核笔记本电脑的速度得到了很好的提升:
> bench::mark(parallelDraws(1e8), serialDraws(1e8), check = FALSE, min_iterations = 10)[,1:5]
# A tibble: 2 x 5
expression min median `itr/sec` mem_alloc
<bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt>
1 parallelDraws(1e+08) 326ms 343ms 2.91 763MB
2 serialDraws(1e+08) 757ms 770ms 1.30 763MB
因此,使用并行计算是否有意义很大程度上取决于所需的随机抽奖次数。
顺便说一句,在评论中提到了我的dqrng package。该软件包还使用Ziggurat方法进行常规(和指数)绘制,并结合了非常快的64位RNG,使其具有与常规绘制相比可比的RcppZiggurat串行速度。另外,使用的RNG为ment for parallel computation,即不需要通过使用不同的种子来获得不重叠的随机流。