Модель прокси R cpp и распределение памяти R - PullRequest
5 голосов
/ 23 апреля 2020

Я пытаюсь лучше понять, как работает модель прокси R cpp.

Для этого рассмотрим следующую задачу: отобрать экспоненциальные случайные величины и что-то сделать с результатом. Наивной реализацией R cpp может быть

NumericMatrix rmexp1(int n, int d) {
  NumericMatrix out(n, d);
  NumericVector values;
  for (int k=0; k<n; k++) {
    values = Rcpp::rexp(d);
    // do something with values 
    out(k, _) = values;
  }
  return out;
}

Верны ли следующие операторы?

  • На каждой итерации, в l # 5, Rcpp::rexp выделяет место для нового Вектор R, затем values сохраняет ссылку на это и отбрасывает ранее сохраненную ссылку.
  • В l # 7 значения в values жестко копируются в out(k, _), так как слева и справа типы данных со стороны разные.
  • Если это так, для объектов в R выделяется много памяти без реальной необходимости. Следует ли этого избегать, если скорость является проблемой?

1 Ответ

5 голосов
/ 24 апреля 2020

Давайте подойдем к этому экспериментально. Сколько памяти выделяется R и сколько времени это занимает? Во-первых, давайте используем вашу функцию и запускаем ее с разными аргументами. Я обертываю это в bench::mark, так как это дает мне измерения оперативной памяти и процессора:

> bench::mark(rmexp1(100, 10),
+             rmexp1(100, 100),
+             rmexp1(100, 1000),
+             rmexp1(100, 10000),
+             check = FALSE)
#> # A tibble: 4 x 13
#>   expression              min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>         <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 10)     46.93µs  52.61µs   16307.    10.35KB     8.24  7918     4
#> 2 rmexp1(100, 100)   381.41µs 538.42µs    1786.      3.9MB     4.14   863     2
#> 3 rmexp1(100, 1000)    4.83ms   5.08ms     187.     1.53MB     8.68    86     4
#> 4 rmexp1(100, 10000)  59.85ms  63.19ms      15.5   15.27MB     5.17     6     2
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>

Неудивительно, что большая матрица занимает больше времени и требует больше памяти. Кроме того, выделенная память примерно в два раза больше памяти, необходимой для выходной матрицы. Так что да, мы выделяем больше памяти, чем необходимо здесь.

Это критично для производительности? Это зависит. В конце концов, вы создаете случайные переменные с экспоненциальным распределением, которое занимает конечное время. Кроме того, вы делаете неуказанные вычисления в do something with values, которые могут занять еще больше времени. Давайте избавимся от создания случайных переменных, используя альтернативные функции, которые выделяют память только с инициализацией или без ее обнуления:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
NumericMatrix rmzero(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = Rcpp::NumericVector(d);
        // do something with values 
        out(k, _) = values;
    }
    return out;
}

// [[Rcpp::export]]
NumericMatrix rmnoinit(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = Rcpp::NumericVector(Rcpp::no_init(d));
        // do something with values 
        out(k, _) = values;
    }
    return out;
}

С bench::mark мы получаем:

> bench::mark(rmexp1(100, 1000),
+             rmzero(100, 1000),
+             rmnoinit(100, 1000),
+             check = FALSE)
#> # A tibble: 3 x 13
#>   expression               min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>          <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 1000)     4.83ms   5.05ms      190.    1.53MB     8.72    87     4
#> 2 rmzero(100, 1000)   509.74µs 562.24µs     1510.    1.53MB    60.4    525    21
#> 3 rmnoinit(100, 1000) 404.24µs 469.43µs     1785.    1.53MB    53.8    664    20
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>

Так примерно только 1/10 времени выполнения вашей функции связано с выделением памяти и другими накладными расходами. Остальное происходит от случайных переменных.

Если генерация случайных переменных является фактическим узким местом в вашем коде, вас может заинтересовать мой пакет dqrng :

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::depends(dqrng)]]
#include <dqrng.h>
// [[Rcpp::export]]
NumericMatrix rmdqexp1(int n, int d) {
    NumericMatrix out(n, d);
    NumericVector values;
    for (int k=0; k<n; k++) {
        values = dqrng::dqrexp(d);
        // do something with values 
        out(k, _) = values;
    }
    return out;
}

С bench::mark мы получаем:

> bench::mark(rmexp1(100, 1000),
+             rmdqexp1(100, 1000),
+             check = FALSE)
#> # A tibble: 2 x 13
#>   expression             min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>          <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 rmexp1(100, 1000)   3.69ms 5.03ms      201.    1.53MB     6.36    95     3
#> 2 rmdqexp1(100, 1000) 1.09ms 1.21ms      700.    1.65MB    22.6    310    10
#> # … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#> #   time <list>, gc <list>

Можно сэкономить немного времени, используя более быстрый генератор случайных чисел.

...