Performance of runif

Performance of runif

本文关键字:runif of Performance      更新时间:2023-10-16

我正在研究一个针对特定问题的自定义引导算法,因为我想要大量的复制,所以我确实关心性能。在这方面,我对如何正确使用runif有一些疑问。我知道我可以自己运行基准测试,但c++优化往往是困难的,我也想了解任何差异的原因。

第一个问题是:

第一个代码块比第二个代码块快吗?

for (int i = 0; i < n_boot; i++) {
  new_random = runif(n);  //new_random is pre-allocated in class
  // do something with the random numbers
}

for (int i = 0; i < n_boot; i++) {
  NumericVector new_random = runif(n);
  // do something with the random numbers
}

这可能归结为runif是否填充左侧,或者是否分配并传递一个新的NumericVector。

第二个问题:

如果两个版本都分配一个新的向量,我可以通过在标量模式下每次生成一个随机数来改进事情吗?

如果您想知道,内存分配占用了相当大一部分处理时间。通过优化其他不必要的内存分配,我减少了30%的运行时间,所以这很重要。

我设置了以下struct来尝试准确地代表您的场景&促进基准测试:

#include <Rcpp.h>
// [[Rcpp::plugins(cpp11)]]
struct runif_test {
  size_t runs;
  size_t each;
  runif_test(size_t runs, size_t each)
  : runs(runs), each(each)
  {}
  // Your first code block
  void pre_init() {
    Rcpp::NumericVector v = no_init();
    for (size_t i = 0; i < runs; i++) {
      v = Rcpp::runif(each);
    }
  }
  // Your second code block
  void post_init() {
    for (size_t i = 0; i < runs; i++) {
      Rcpp::NumericVector v = Rcpp::runif(each);
    }
  }
  // Generate 1 draw at a time  
  void gen_runif() {
    Rcpp::NumericVector v = no_init();
    for (size_t i = 0; i < runs; i++) {
      std::generate_n(v.begin(), each, []() -> double {
        return Rcpp::as<double>(Rcpp::runif(1));
      });
    }
  }
  // Reduce overhead of pre-allocated vector
  inline Rcpp::NumericVector no_init() {
    return Rcpp::NumericVector(Rcpp::no_init_vector(each));
  } 
};

,我对以下导出函数进行了基准测试:

// [[Rcpp::export]]
void do_pre(size_t runs, size_t each) {
  runif_test obj(runs, each);
  obj.pre_init();
}
// [[Rcpp::export]]
void do_post(size_t runs, size_t each) {
  runif_test obj(runs, each);
  obj.post_init();
}
// [[Rcpp::export]]
void do_gen(size_t runs, size_t each) {
  runif_test obj(runs, each);
  obj.gen_runif();
}

以下是我得到的结果:

R>  microbenchmark::microbenchmark(
    do_pre(100, 10e4)
    ,do_post(100, 10e4)
    ,do_gen(100, 10e4)
    ,times=100L)
Unit: milliseconds
                 expr      min       lq      mean   median        uq       max neval
  do_pre(100, 100000) 109.9187 125.0477  145.9918 136.3749  152.9609  337.6143   100
 do_post(100, 100000) 103.1705 117.1109  132.9389 130.4482  142.7319  204.0951   100
  do_gen(100, 100000) 810.5234 911.3586 1005.9438 986.8348 1062.7715 1501.2933   100

R>  microbenchmark::microbenchmark(
    do_pre(100, 10e5)
    ,do_post(100, 10e5)
    ,times=100L)
Unit: seconds
                  expr      min       lq     mean   median       uq      max neval
  do_pre(100, 1000000) 1.355160 1.614972 1.740807 1.723704 1.815953 2.408465   100
 do_post(100, 1000000) 1.198667 1.342794 1.443391 1.429150 1.519976 2.042511   100

那么,假设我解释/准确地表达了你的第二个问题,

如果两个版本都分配了一个新的向量,我可以通过在标量模式下一次生成一个随机数?

对于我的gen_runif()成员函数,我认为我们可以自信地说这不是最佳方法-比其他两个函数慢约7.5倍。

更重要的是,为了解决你的第一个问题,似乎只初始化&为Rcpp::runif(n)的输出分配一个新的NumericVector。我当然不是c++专家,但我相信第二种方法(给一个新的局部对象赋值)比第一种方法快,因为省略了复制。在第二种情况下,它看起来好像正在创建两个对象——=v左侧的对象和一个(临时的?)对象,该对象位于=的右侧,它是Rcpp::runif()的结果。但实际上,编译器很可能会优化这个不必要的步骤—我认为这在我链接的文章中的这一段中得到了解释:

移动未绑定到任何引用的无名临时对象时或者复制到相同类型的对象中……省略复制/移动。当这是暂时的构造时,它直接在存储中构造否则将被移动或复制到。

这是,至少,我是这样解释结果的。希望更精通英语的人能证实/否认/纠正这个结论。

添加到@nrussell的答案与一些实现细节…

使用来源,卢克!在这里肯定适用,所以让我们看看Rcpp::runif在这里的实现:

inline NumericVector runif( int n, double min, double max ){
    if (!R_FINITE(min) || !R_FINITE(max) || max < min) return NumericVector( n, R_NaN ) ;
    if( min == max ) return NumericVector( n, min ) ;
    return NumericVector( n, stats::UnifGenerator( min, max ) ) ;
}

我们看到NumericVector的一个有趣的构造函数被stats::UnifGenerator对象调用。该类的定义在这里:

    class UnifGenerator__0__1 : public ::Rcpp::Generator<double> {
    public:
        UnifGenerator__0__1() {}
        inline double operator()() const {
            double u;
            do {u = unif_rand();} while (u <= 0 || u >= 1);
            return u;
        }
    } ;

所以,那个类只是一个函子——它实现了operator(),所以那个类的对象可以被"调用"。

最后,相关的NumericVector构造函数在这里:
template <typename U>
Vector( const int& size, const U& u) {
    RCPP_DEBUG_2( "Vector<%d>( const int& size, const U& u )", RTYPE, size )
    Storage::set__( Rf_allocVector( RTYPE, size) ) ;
    fill_or_generate( u ) ;
}

fill_or_generate函数最终会在这里调度:

template <typename T>
inline void fill_or_generate__impl( const T& gen, traits::true_type) {
    iterator first = begin() ;
    iterator last = end() ;
    while( first != last ) *first++ = gen() ;
}

所以我们可以看到,提供了一个(模板化的)生成器函数来填充向量,并且使用gen对象的相应operator()来填充向量——即,在本例中,stats::UnifGenerator对象。

那么,问题是,这一切是如何在这次通话中结合在一起的?

NumericVector x = runif(10);

由于某些原因,我总是忘记这一点,但我认为这基本上是xrunif(10)调用的结果复制构造的,但这一点也被@nrussell详细阐述了。但是,我的理解是:

  1. runifrunif元素生成一个长度为10的NumericVector——将这个临时的右边对象命名为tmp
  2. x的复制构造与前面提到的tmp相同。

我相信编译器将能够省略该复制构造,因此x实际上是直接从runif(10)的结果构造的,因此应该是有效的(至少,在任何合理的优化级别),但我可能是错误的....