Почему реализация Rcpp в моем примере намного медленнее, чем функция R? - PullRequest
6 голосов
/ 07 апреля 2019

У меня есть некоторый опыт работы с C ++ и R, но я новичок в Rcpp. Недавно я имел огромный успех, используя Rcpp в некоторых из моих предыдущих проектов, поэтому решил применить его к новому проекту. Я был удивлен, что мой код Rcpp мог быть намного медленнее, чем соответствующая R-функция. Я попытался упростить свою функцию R, чтобы выяснить причину, но не могу найти никакой подсказки. Ваша помощь и комментарии приветствуются!

Основная функция R для сравнения реализаций R и Rcpp:

main <- function(){

  n <- 50000
  Delta <- exp(rnorm(n))
  delta <- exp(matrix(rnorm(n * 5), nrow = n))
  rx <- matrix(rnorm(n * 20), nrow = n)
  print(microbenchmark(c1 <- test(Delta, delta, rx), times = 500))
  print(microbenchmark(c2 <- rcpp_test(Delta, delta, rx), times = 500))

  identical(c1, c2)
  list(c1 = c1, c2 = c2)
}

R реализация:

test <- function(Delta, delta, rx){

  const <- list()
  for(i in 1:ncol(delta)){
    const[[i]] <- rx * (Delta / (1 + delta[, i]))
  }

  const

}

Реализация Rcpp:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
List rcpp_test(NumericVector Delta, 
               NumericMatrix delta, 
               NumericMatrix rx) {

  int n = Delta.length();
  int m = rx.ncol();

  List c; 
  NumericMatrix c1;
  for(int i = 0; i < delta.ncol(); ++i){
    c1 = NumericMatrix(n, m);
    for(int k = 0; k < n; ++k){
      double tmp = Delta[k] / (1 + delta(k, i));
      for(int j = 0; j < c1.ncol(); ++j){
        c1(k, j) = rx(k, j) * tmp; 
      }
    }
    c.push_back(c1);
  }

  return c;

}

Я понимаю, что нет гарантии повышения эффективности с помощью Rcpp, но, учитывая простой пример, который я показываю здесь, я не понимаю, почему код Rcpp работает так медленно.

Unit: milliseconds
                         expr      min       lq     mean   median       uq      max neval
 c1 <- test(Delta, delta, rx) 13.16935 14.19951 44.08641 30.43126 73.78581 115.9645   500
Unit: milliseconds
                              expr      min       lq     mean  median       uq      max neval
 c2 <- rcpp_test(Delta, delta, rx) 143.1917 158.7481 171.6116 163.413 173.7677 247.5495   500

В идеале rx - это список матриц в моем проекте. Переменная i в цикле for будет использоваться для выбора элемента для вычислений. Сначала я подозревал, что передача List в Rcpp может иметь большие накладные расходы, поэтому в этом примере я предположил, что rx является фиксированной матрицей, используемой для всех i. Похоже, что это не причина медлительности.

Ответы [ 2 ]

7 голосов
/ 07 апреля 2019

Ваш R-код кажется более или менее оптимальным, то есть вся реальная работа выполняется в скомпилированном коде. Для кода C ++ основная проблема, которую я мог найти, - это вызов c1.ncol() в тесном цикле. Если я заменю это на m, решение C ++ будет почти таким же быстрым, как и R. Если я добавлю RcppArmadillo в смесь, я получу очень компактный синтаксис, но не быстрее, чем чистый код Rcpp. Для меня это показывает, что может быть действительно трудно победить хорошо написанный код R:

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

// [[Rcpp::export]]
List arma_test(const arma::vec& Delta,
           const arma::mat& delta,
           const arma::mat& rx) {
  int l = delta.n_cols;
  List c(l);

  for (int i = 0; i < l; ++i) {
    c(i) = rx.each_col() % (Delta / (1 + delta.col(i)));
  }

  return c;  
}

// [[Rcpp::export]]
List rcpp_test(NumericVector Delta, 
               NumericMatrix delta, 
               NumericMatrix rx) {

  int n = Delta.length();
  int m = rx.ncol();

  List c(delta.ncol()); 
  NumericMatrix c1;
  for(int i = 0; i < delta.ncol(); ++i){
    c1 = NumericMatrix(n, m);
    for(int k = 0; k < n; ++k){
      double tmp = Delta[k] / (1 + delta(k, i));
      for(int j = 0; j < m; ++j){
        c1(k, j) = rx(k, j) * tmp; 
      }
    }
    c(i) = c1;
  }

  return c;

}

/*** R
test <- function(Delta, delta, rx){

  const <- list()
  for(i in 1:ncol(delta)){
    const[[i]] <- rx * (Delta / (1 + delta[, i]))
  }

  const

}

n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
bench::mark(test(Delta, delta, rx),
            arma_test(Delta, delta, rx),
            rcpp_test(Delta, delta, rx))
 */

Выход:

# A tibble: 3 x 14
  expression     min    mean  median     max `itr/sec` mem_alloc  n_gc n_itr
  <chr>      <bch:t> <bch:t> <bch:t> <bch:t>     <dbl> <bch:byt> <dbl> <int>
1 test(Delt…  84.3ms  85.2ms  84.9ms  86.6ms     11.7     44.9MB     2     4
2 arma_test… 106.5ms 107.7ms 107.7ms 108.9ms      9.28    38.1MB     3     2
3 rcpp_test… 101.9ms 103.2ms 102.2ms 106.6ms      9.69    38.1MB     1     4
# … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#   time <list>, gc <list>

Я также явно инициализировал список вывода до требуемого размера, избегая push_back, но это не имело большого значения. С векторными структурами данных из Rcpp вам определенно следует избегать использования push_back, поскольку при каждом расширении вектора будет создаваться копия.

3 голосов
/ 08 апреля 2019

Я хотел бы добавить к отличному ответу @ RalfStubner.

Вы заметите, что мы делаем много выделений в первом цикле for (т.е. c1 = NumerMatrix(n, m)). Это может быть дорого, так как мы инициализируем каждый элемент в 0 в дополнение к выделению памяти. Для повышения эффективности мы можем изменить это на следующее:

NumericMatrix c1 = no_init_matrix(n, m)

Я также добавил ключевое слово const, где это возможно. Это спорный вопрос, если делать это позволяет компилятору оптимизировать определенные части кода, но я все еще добавить, где я могу для ясности кода (т.е. «Я не хочу эту переменную, чтобы изменить» ). С этим мы имеем:

// [[Rcpp::export]]
List rcpp_test_modified(const NumericVector Delta, 
                        const NumericMatrix delta, 
                        const NumericMatrix rx) {

    int n = Delta.length();
    int m = rx.ncol();
    int dCol = delta.ncol();

    List c(dCol);

    for(int i = 0; i < dCol; ++i) {
        NumericMatrix c1 = no_init_matrix(n, m);

        for(int k = 0; k < n; ++k) {
            const double tmp = Delta[k] / (1 + delta(k, i));

            for(int j = 0; j < m; ++j) {
                c1(k, j) = rx(k, j) * tmp; 
            }
        }

        c[i] = c1;
    }

    return c;

}

А вот некоторые тесты (Armadillo решение не учтено):

bench::mark(test(Delta, delta, rx),
            rcpp_test_modified(Delta, delta, rx),
            rcpp_test(Delta, delta, rx))
# A tibble: 3 x 14
  expression     min   mean  median    max `itr/sec` mem_alloc  n_gc n_itr total_time result memory time 
  <chr>      <bch:t> <bch:> <bch:t> <bch:>     <dbl> <bch:byt> <dbl> <int>   <bch:tm> <list> <list> <lis>
1 test(Delt… 12.27ms 17.2ms 14.56ms 29.5ms      58.1    41.1MB    13     8      138ms <list… <Rpro… <bch…
2 rcpp_test…  7.55ms 11.4ms  8.46ms   26ms      87.8    38.1MB    16    21      239ms <list… <Rpro… <bch…
3 rcpp_test… 10.36ms 15.8ms 13.64ms 28.9ms      63.4    38.1MB    10    17      268ms <list… <Rpro… <bch…
# … with 1 more variable: gc <list>

И мы видим улучшение 50% с версией Rcpp.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...