Законно ли использовать Rcpp для ускорения замены элементов списков и векторов в итеративном алгоритме? - PullRequest
0 голосов
/ 04 сентября 2018

Контекст

В последнее время я работал над итерационным алгоритмом, где каждая итерация n зависит от итерации n-1. Во время каждой итерации большая часть времени вычислений занимает подстановка и / или замена элементов векторов, списков или таблиц данных (N> 10 ^ 6).

Недавно я наткнулся на Rcpp и, немного поиграв с ним, обнаружил, что замена элемента k векторов или списков может быть ускорена на два-три порядка (несколько тестов производительности ниже).

Однако при использовании кода поднабора Rcpp в цикле for и while R кажется нестабильным, и сеанс прерывается в случайных точках без намека на то, что пошло не так.

Вопрос

Мой вопрос: законно ли использование Rcpp или оно может привести к проблемам, о которых я не знаю?

Пример

Ниже приведен код Rcpp, который я использую, и несколько тестов. В целом алгоритм должен вызывать замещающие функции ~ 5,5 миллиардов раз, а функции подмножества ~ 50 миллиардов раз.

Обратите внимание, что замена элементов списков и двойных векторов быстрее при использовании Rcpp, , в то время как для целочисленных векторов предпочтительны базовые R-решения ( тест 1 ); Таблица данных - это хороший вариант для замены элементов, но если вам нужно повторно вводить подмножества для доступа к его элементам, векторный подход намного быстрее ( тест 2 ).

Функции:

#include <Rcpp.h>
using namespace Rcpp;


// [[Rcpp::export]]

void assign_list(List x, int k, NumericVector new_element){
  x[k-1] = new_element;
}

// [[Rcpp::export]]
void assign_dbl(NumericVector x, int k, double y){
  x[k-1] = y;
}

// [[Rcpp::export]]
void assign_int(IntegerVector x, int k, int y){
  x[k-1] = y;
}

Тесты:

Входы

set.seed(747474)

int <- 1:10^7
dou <- rnorm(10^7, 1000, 300)
l   <- lapply(sample(5:20, 10^7,  replace = T), rnorm, mean = 1000, sd = 300)
dt  <- data.table(int = int, dou = dou, l = l)

i <- 999999
z <- 2222
k <- 30000
s <- 552877

1)

Unit: nanoseconds
                                     expr      min       lq        mean      median        uq       max neval
                             int[i] <- -1L     488     2439  36938108.9      4146.0  15651119 799720107    30
                             dou[i] <- -1      732     3170  19101960.4   6609193.5  16187500 212369197    30
                             l[i]   <- -1      489     3902 159442538.1 186035314.5 227131872 618326686    30
                               assign_int 19853910 22028692  81055363.5  24665494.0  39352345 872241539    30
                               assign_dbl     1220     5852     48023.2      8534.5     13167   1158828    30
                              assign_list     1464     6828     52866.9     10850.5     13411   1243430    30
 dt[k, ':=' (int = -1, dou = -1, l = -1)]   206020   340116    481850.0    425326.5    529312   1414341    30

2)

microbenchmark(times = 30L,

               "subset vector + list" = {int[s]; dou[s]; l[s]},
               "subset datatable"     = {dt[s, int]; dt[s, dou]; dt[s, l]})

Unit: nanoseconds
                 expr    min     lq       mean   median     uq     max neval
 subset vector + list    488    488   1715.533   1585.5   2926    4389    30
     subset datatable 563688 574417 719304.467 600138.0 875765 1308040    30

1 Ответ

0 голосов
/ 04 сентября 2018

Это очень опасно из-за показанного здесь побочного эффекта, в котором x и y изменены, даже если вы только передаете x в функцию Rcpp

> x <- y <- 1:10
> assign_int(x, 1, 2)
> y
 [1]  2  2  3  4  5  6  7  8  9 10

Кажется, не быстрее; для этих функций

f0 <- function(x) {
    for (i in seq_along(x))
        x[i] = -i
}

f1 <- function(x) {
    for (i in seq_along(x))
        assign_int(x, i, -i)
}

у меня

> int <- 1:10^5
> microbenchmark(f0(int), f1(int), times=5)
Unit: milliseconds
    expr       min        lq      mean    median        uq       max neval
 f0(int)  14.78777  14.80264  15.05683  15.03138  15.17678  15.48556     5
 f1(int) 659.67346 669.00095 672.93343 670.48917 676.16930 689.33429     5

В вашем тесте int[i] <- 1, '1' является числовым (двойным) значением, поэтому вы приводите к двойному вектору (отметьте class(int) после назначения), вызывая полную копию. Используйте int[i] <- 1L, чтобы правая часть была целым числом.

> int0 <- int1 <- 1:10^7
> microbenchmark(int0[1] <- 1, int1[1] <- 1L)
Unit: microseconds
          expr   min    lq      mean median     uq       max neval
  int0[1] <- 1 1.047 1.102 1770.9911  1.143 1.2650 176960.52   100
 int1[1] <- 1L 1.105 1.176  339.4264  1.213 1.2655  33815.97   100
> class(int0)
[1] "numeric"
> class(int1)
[1] "integer"

Обновление только одного элемента в качестве эталона является дорогостоящим в базе R, потому что оно вызывает копию вектора при каждом назначении; в f0() копия происходит только один раз. На первой итерации R делает копию, потому что знает, что на вектор целочисленных значений ссылаются как минимум два символа (аргумент функции int и символ, используемый в функции x), поэтому он создает копия памяти и присваивает ее x внутри функции. Это делается для того, чтобы избежать побочного эффекта, видимого в вашем коде Rcpp (то есть, чтобы избежать изменения int). В следующий раз в цикле R распознается, что только один символ ссылается на вектор, поэтому замена выполняется без копирования.

Обратите внимание, что

> int <- 1:10^5
> f1(int)
> head(int)
[1] -1 -2 -3 -4 -5 -6

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

Также есть несколько способов замедления итерационных циклов, например,

f2 <- function(x) {
    for (i in seq_along(x)) {
        x[i] = -i
        y <- x
    }
}

f3 <- function(x) {
    result <- integer()
    for (i in seq_along(x))
        result <- c(result, -i)
}

с

> int <- 1:10^3
> microbenchmark(f0(int), f2(int), f3(int), times = 1)
Unit: microseconds
    expr      min       lq     mean   median       uq      max neval
 f0(int)  150.507  150.507  150.507  150.507  150.507  150.507     1
 f2(int)  667.201  667.201  667.201  667.201  667.201  667.201     1
 f3(int) 4379.005 4379.005 4379.005 4379.005 4379.005 4379.005     1

f2() заставляет R делать копию x каждый раз через цикл (чтобы избежать побочного эффекта изменения y). f3() копирует вектор длиной 0, 1, 2, 3, ... n - 1 (где n = length(x)) на последовательных итерациях, что приводит к копированию n * (n - 1) / 2 элементов, и алгоритм, который масштабируется как квадрат длина x.

Общий принцип применим и к другим типам, включая списки с

f0l <- function(x) {
    for (i in seq_along(x))
        x[[i]] <- i
    x
}

f1l <- function(x) {
    for (i in seq_along(x))
        assign_list(x, i, i)
}

ведет к

> set.seed(123)
> l0   <- lapply(sample(5:20, 10^6,  replace = T), rnorm, mean = 1000, sd = 300)
> set.seed(123)
> l1   <- lapply(sample(5:20, 10^6,  replace = T), rnorm, mean = 1000, sd = 300)
> microbenchmark(f0l(l0), f1l(l1), times=1)
Unit: milliseconds
    expr       min        lq      mean    median        uq       max neval
 f0l(l0)  239.9865  239.9865  239.9865  239.9865  239.9865  239.9865     1
 f1l(l1) 6767.9172 6767.9172 6767.9172 6767.9172 6767.9172 6767.9172     1
...