在Rcpp-OpenMP、RcppParallel或RcppThread中对矩阵的所有列或所有行执行quickselect的最快多线程方式

在Rcpp-OpenMP、RcppParallel或RcppThread中对矩阵的所有列或所有行执行quickselect的最快多线程方式,r,rcpp,percentile,quickselect,rcppparallel,R,Rcpp,Percentile,Quickselect,Rcppparallel,我使用这个Rcpp代码对一个值向量进行分析,即在O(n)时间内从向量中获取第k个最大元素(我将其保存为qselect.cpp): [这看起来已经很快了,但我需要在我的应用程序中花费数百万时间来完成这项工作] 现在我想修改这个Rcpp函数,以便它对R矩阵的所有列或所有行执行多线程快速选择,并将结果作为向量返回。由于我是Rcpp的一个新手,我想听听一些建议,哪种框架可能最快,哪种框架最容易编码(它必须能够轻松跨平台工作,我需要很好地控制要使用的线程数量)。使用,还是?或者更好——如果有人能够演示一种

我使用这个Rcpp代码对一个值向量进行分析,即在O(n)时间内从向量中获取第k个最大元素(我将其保存为
qselect.cpp
):

[这看起来已经很快了,但我需要在我的应用程序中花费数百万时间来完成这项工作]


现在我想修改这个Rcpp函数,以便它对R矩阵的所有列或所有行执行多线程快速选择,并将结果作为向量返回。由于我是Rcpp的一个新手,我想听听一些建议,哪种框架可能最快,哪种框架最容易编码(它必须能够轻松跨平台工作,我需要很好地控制要使用的线程数量)。使用,还是?或者更好——如果有人能够演示一种快速而优雅的方法来实现这一点?

是的,这将是多线程变体的候选——但正如RcppParallel文档所告诉您的,并行代码的一个要求是非R内存,在这里我们以高效的零拷贝方式使用RcppArmadillo,这意味着它是R内存

因此,您可能需要权衡并行执行所使用的额外数据副本(例如,
RMatrix
type RcppParallel)


但由于您的算法简单且按列运行,您也可以在上述函数中使用单个OpenMP循环进行实验:将矩阵传递给它,使用
#pragma for
在列上循环

按照下面的建议,我尝试了使用OpenMP进行多线程处理,这似乎可以在我的笔记本电脑上使用8个线程来提供不错的加速效果。我将我的
qselect.cpp
文件修改为:

// [[Rcpp::depends(RcppArmadillo)]]
#define RCPP_ARMADILLO_RETURN_COLVEC_AS_VECTOR
#include <RcppArmadillo.h>
using namespace arma;

// [[Rcpp::export]]
double qSelect(arma::vec& x, const int k) {

  // ARGUMENTS
  // x: vector to find k-th largest element in
  // k: k-th statistic to look up

  // safety copy since nth_element modifies in place
  arma::vec y(x.memptr(), x.n_elem);

  // partially sorts y
  std::nth_element(y.begin(), y.begin() + k - 1, y.end());

  // the k-th largest value
  const double kthValue = y(k-1);

  return kthValue;

}


// [[Rcpp::export]]
arma::vec qSelectMbycol(arma::mat& M, const int k) {

  // ARGUMENTS
  // M: matrix for which we want to find the k-th largest elements of each column
  // k: k-th statistic to look up

  arma::mat Y(M.memptr(), M.n_rows, M.n_cols);
  // we apply over columns
  int c = M.n_cols;
  arma::vec out(c);
  int i;
  for (i = 0; i < c; i++) {
      arma::vec y = Y.col(i);
      std::nth_element(y.begin(), y.begin() + k - 1, y.end());
      out[i] = y(k-1); // the k-th largest value of each column
  }

  return out;

}

#include <omp.h>
// [[Rcpp::plugins(openmp)]]

// [[Rcpp::export]]
arma::vec qSelectMbycolOpenMP(arma::mat& M, const int k, int nthreads) {

  // ARGUMENTS
  // M: matrix for which we want to find the k-th largest elements of each column
  // k: k-th statistic to look up
  // nthreads: nr of threads to use

  arma::mat Y(M.memptr(), M.n_rows, M.n_cols);
  // we apply over columns
  int c = M.n_cols;
  arma::vec out(c);
  int i;
  omp_set_num_threads(nthreads);
#pragma omp parallel for shared(out) schedule(dynamic,1)
  for (i = 0; i < c; i++) {
    arma::vec y = Y.col(i);
    std::nth_element(y.begin(), y.begin() + k - 1, y.end());
    out(i) = y(k-1); // the k-th largest value of each column
  }

  return out;

}
我感到惊讶的是,即使不使用多线程(qSelectMbycol函数),在Rcpp中应用的速度也提高了2倍,而OpenMP多线程(qSelectMbycolOpenMP)的速度又提高了2倍

不过,欢迎对可能的代码优化提出任何建议


对于小型
n
n
那么,你是说OpenMP将是这里唯一一个我可以轻松避免制作额外数据副本的框架吗?OpenMP实际上已经在CRAN软件包中普遍使用了吗?(我感觉不是,而且对Mac用户有点害怕,因为我相信他们有时在设置OpenMP时会遇到问题)我从来不会说任何绝对的话,因为只有小淘气才有把握地了解一切的好处:)你可能想要基准测试和尝试不同的方法。在我看来,复制(这将是一件安全的事情)的成本可能足以让并行代码变得不值得。但话说回来。。。Conrad还在Armadillo内部使用OpenMP,在一个(假定为呼叫固定)数据块的列上循环似乎是安全的。因此,您可以避免复制
SEXP
数据。但是你知道证据和布丁…谢谢你的建议!我在我的问题中做了一个编辑,现在包含了一些OpenMP代码-这是我第一次尝试,我真的不是专家-所以如果你看到任何可以做得更好的事情,请告诉我!但我通过在Rcpp中应用程序获得了大约2倍的速度提升,并通过OpenMP多线程进一步获得了2倍的速度提升……尽管如此,我的代码中仍然存在一些复制,我认为正因为如此,对于小n来说,OpenMP版本不会更快。如果你碰巧知道如何避免这种复制,请告诉我!顺便说一句,
microbenchmark()
常用的方法是将两个(相互竞争的)表达式放到一个调用中:
microbenchmark(qSelect(x,k),sort(x,partial=k)[k])
n = 50000
set.seed(1)
x = rnorm(n=n, mean=100, sd=20)
tau = 0.01 # desired percentile
k = tau*n+1 # here we will get the 6th largest element
library(Rcpp)
Rcpp::sourceCpp('qselect.cpp')
library(microbenchmark)
microbenchmark(qSelect(x,k)) # 53.32917, 548 µs
microbenchmark(sort(x, partial=k)[k]) # 53.32917, 694 µs = pure R solution
// [[Rcpp::depends(RcppArmadillo)]]
#define RCPP_ARMADILLO_RETURN_COLVEC_AS_VECTOR
#include <RcppArmadillo.h>
using namespace arma;

// [[Rcpp::export]]
double qSelect(arma::vec& x, const int k) {

  // ARGUMENTS
  // x: vector to find k-th largest element in
  // k: k-th statistic to look up

  // safety copy since nth_element modifies in place
  arma::vec y(x.memptr(), x.n_elem);

  // partially sorts y
  std::nth_element(y.begin(), y.begin() + k - 1, y.end());

  // the k-th largest value
  const double kthValue = y(k-1);

  return kthValue;

}


// [[Rcpp::export]]
arma::vec qSelectMbycol(arma::mat& M, const int k) {

  // ARGUMENTS
  // M: matrix for which we want to find the k-th largest elements of each column
  // k: k-th statistic to look up

  arma::mat Y(M.memptr(), M.n_rows, M.n_cols);
  // we apply over columns
  int c = M.n_cols;
  arma::vec out(c);
  int i;
  for (i = 0; i < c; i++) {
      arma::vec y = Y.col(i);
      std::nth_element(y.begin(), y.begin() + k - 1, y.end());
      out[i] = y(k-1); // the k-th largest value of each column
  }

  return out;

}

#include <omp.h>
// [[Rcpp::plugins(openmp)]]

// [[Rcpp::export]]
arma::vec qSelectMbycolOpenMP(arma::mat& M, const int k, int nthreads) {

  // ARGUMENTS
  // M: matrix for which we want to find the k-th largest elements of each column
  // k: k-th statistic to look up
  // nthreads: nr of threads to use

  arma::mat Y(M.memptr(), M.n_rows, M.n_cols);
  // we apply over columns
  int c = M.n_cols;
  arma::vec out(c);
  int i;
  omp_set_num_threads(nthreads);
#pragma omp parallel for shared(out) schedule(dynamic,1)
  for (i = 0; i < c; i++) {
    arma::vec y = Y.col(i);
    std::nth_element(y.begin(), y.begin() + k - 1, y.end());
    out(i) = y(k-1); // the k-th largest value of each column
  }

  return out;

}
n = 50000
set.seed(1)
x = rnorm(n=n, mean=100, sd=20)
M = matrix(rnorm(n=n*10, mean=100, sd=20), ncol=10)
tau = 0.01 # desired percentile
k = tau*n+1 # we will get the 6th smallest element
library(Rcpp)
Rcpp::sourceCpp('qselect.cpp')
library(microbenchmark
microbenchmark(apply(M, 2, function (col) sort(col, partial=k)[k]),
               apply(M, 2, function (col) qSelect(col,k)),
               qSelectMbycol(M,k),
               qSelectMbycolOpenMP(M,k,nthreads=8))[,1:4]

Unit: milliseconds
                                                 expr      min       lq      mean    median        uq        max neval cld
 apply(M, 2, function(col) sort(col, partial = k)[k]) 8.937091 9.301237 11.802960 11.828665 12.718612  43.316107   100   b
           apply(M, 2, function(col) qSelect(col, k)) 6.757771 6.970743 11.047100  7.956696  9.994035 133.944735   100   b
                                  qSelectMbycol(M, k) 5.370893 5.526772  5.753861  5.641812  5.826985   7.124698   100  a 
              qSelectMbycolOpenMP(M, k, nthreads = 8) 2.695924 2.810108  3.005665  2.899701  3.061996   6.796260   100  a 
Unit: microseconds
                                                 expr     min       lq      mean   median       uq      max neval cld
 apply(M, 2, function(col) sort(col, partial = k)[k]) 310.477 324.8025 357.47145 337.8465 361.5810 1782.885   100   c
           apply(M, 2, function(col) qSelect(col, k)) 103.921 114.8255 141.59221 119.3155 131.9315 1990.298   100  b 
                                  qSelectMbycol(M, k)  24.377  32.2885  44.13873  35.2825  39.3440  900.210   100 a  
              qSelectMbycolOpenMP(M, k, nthreads = 8)  76.123  92.1600 130.42627  99.8575 112.4730 1303.059   100  b