Вопросы о производительности data.table: get vs [[]], вычисления на месте - PullRequest
1 голос
/ 27 мая 2020

Рассмотрим следующий фрагмент кода:

foo <- function(dt, num) {
    expect_equal(class(num), "numeric")
    col <- paste("b", num, sep = "_")
    col2 <- paste("b", num + 1, sep = "_")

    condition <- dt$a > 0

    st <- nanotime(Sys.time())
    dt[condition, a := a - get(col) ]
    dt[condition, a := a - get(col2) ]
    et <- nanotime(Sys.time())
    diff <- (et - st) / 1e9
    message(diff)

    st <- nanotime(Sys.time())
    tmp <- dt$a - dt[[col]]
    tmp <- tmp - dt[[col2]]
    dt[condition, a := tmp[condition]]
    et <- nanotime(Sys.time())
    diff <- (et - st) / 1e9
    message(diff)

    st <- nanotime(Sys.time())
    dt[, tmp := a - get(col)]
    dt[, tmp := a - get(col2)]
    dt[condition, a := tmp]
    et <- nanotime(Sys.time())
    diff <- (et - st) / 1e9
    message(diff)
}

dt <- data.table(c = 0, d = 0, e = 0, f = 0, g = 0, h = 0, i = 0, a = -15000:15000, b_1 = 1L, b_2 = 1L)
foo(dt, 1)

Выход 0,002342 0,001131 0,002389

Запросы 1. get(col) медленнее, чем dt[[col]]? 2. Является ли векторизация, какой бы сложной она ни была, лучше выполнять вычисления для всех данных, а не для их разделения? 3. Следует ли проводить серию вычислений вне таблицы данных и в конце задать ее в столбце, а не на месте вычислений?

1 Ответ

3 голосов
/ 27 мая 2020

Если я возьму все внутренние компоненты и запустил с microbenchmark, он выполнит каждое (в случайном порядке) и выдаст хорошую статистику. Я предварительно вычислю tmp и condition,

microbenchmark::microbenchmark(
  a = {
    dt[condition, a := a - get(col) ]
    dt[condition, a := a - get(col2) ]
  },
  b = {
    tmp <- dt$a - dt[[col]]
    tmp <- tmp - dt[[col2]]
    dt[condition, a := tmp[condition]]
  },
  b2 = {
    tmp <- dt$a - dt[[col]]
    tmp <- tmp - dt[[col2]]
    set(dt, i = which(condition), j = "a", value = tmp[condition])
  },
  c = {
    dt[, tmp := a - get(col)]
    dt[, tmp := a - get(col2)]
    dt[condition, a := tmp]
  },
  times = 1000
)
# Unit: microseconds
#  expr      min       lq      mean   median       uq       max neval
#     a 2871.501 2965.951 3429.7118 3058.701 3640.551  9190.800  1000
#     b  660.601  679.701  788.5797  696.451  805.801  3675.201  1000
#    b2  166.001  176.801  251.9144  180.201  187.501 39527.302  1000
#     c 1391.001 1502.901 1633.2692 1530.150 1664.101  3638.701  1000
  1. Похоже, что в вашем исходном наборе кандидатов есть явный победитель: dt[[col]] затмевает их всех . Изменить : однако, как прокомментировал @jangorecki (значительный участник в data.table источник), data.table::set работает еще быстрее.

  2. На самом деле это не очень хорошо протестировано здесь, но это действительно зависит от количества подмножеств и от того, насколько «дороги» вычисления. В этом случае вычисления довольно тривиальны, поэтому я не ожидал большой разницы.

  3. Вы всегда балансируете удобочитаемость и ремонтопригодность со скоростью и эффективностью. В некоторых моих чувствительных к скорости материалах (длина 2-4M) я обычно все делаю в необработанном векторе, но это решение включает несколько факторов, а не только те, которые связаны с data.table. Как только вы начнете получать значительные копии данных (R делает это часто) и многие сильные стороны data.table (группировка, неравенство объединений и т. Д. c), выполнение этого в таблице становится намного быстрее и, что более важно для мне, более ремонтопригодный и читаемый.

...