Подмножество data.table в цикле for выполняется медленнее и требует больше ресурсов - PullRequest
0 голосов
/ 29 января 2019

Работая с пакетом data.table R, я заметил очень высокую загрузку процессора при запуске простого цикла for, который бы поднастраивал набор данных, используя значения из другого data.table.Когда я говорю «высокий уровень использования», я имею в виду 100% всех доступных потоков за все время выполнения цикла.

Интересно то, что использование объекта data.frame для одного и того же процесса занимает в 10 раз меньше времени для того жевыход.И только с одним из ядер на 100%.

Вот мой, мы надеемся, воспроизводимый пример:

chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

test1 <- list()
test2 <- list()

#loop subsetting a data.frame
system.time(
for (i in 1:nrow(df2)) {
  no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
  test1[i] <- no.dim
})

# loop subsetting a data.table using data.table syntax
system.time(
for (i in 1:nrow(dt2)) {
  no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
  test2[i] <- no.dim
})

# is the output the same
identical(test1, test2)

И это вывод:

> #loop subsetting a data.frame
> system.time(
+ for (i in 1:nrow(df2)) {
+   no.dim <- dim(df1[df1$chr == df2[i, 'chr'] & df1$start >= df2[i, 'start'] & df1$end <= df2[i, 'end'], ])[1]
+   test1[i] <- no.dim
+ })
   user  system elapsed 
  2.607   0.004   2.612 
> 
> # loop subsetting a data.table using data.table syntax
> system.time(
+ for (i in 1:nrow(dt2)) {
+   no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
+   test2[i] <- no.dim
+ })
   user  system elapsed 
192.632   0.152  24.398 
> 
> # is the output the same
> identical(test1, test2)
[1] TRUE

Теперь,Я знаю, что, вероятно, есть несколько более эффективных и эффективных способов выполнить одну и ту же задачу, и что я, вероятно, не делаю это способом data.table.Но допустим, по какой-то причине у вас был скрипт, использующий объекты data.frame, и вы хотели быстро переписать его, чтобы вместо него использовать data.table.Приведенный выше подход кажется вполне правдоподобным.

Может ли кто-нибудь воспроизвести такую ​​же ситуацию с замедлением и высокой загрузкой процессора?Это как-то поправимо, сохраняя более или менее тот же процесс подмножества, или его нужно полностью переписать, чтобы эффективно использовать на data.table?

PS: только что протестировал его на машине с Windows и в потокеиспользование было нормальным (один поток работал на 100%), но все еще медленнее.Протестировано на системе, аналогичной моей, дало те же результаты, что и выше.

R version 3.5.1 (2018-07-02)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.10

Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.8.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.8.0

locale:
 [1] LC_CTYPE=C           LC_NUMERIC=C         LC_TIME=C            LC_COLLATE=C        
 [5] LC_MONETARY=C        LC_MESSAGES=C        LC_PAPER=et_EE.UTF-8 LC_NAME=C           
 [9] LC_ADDRESS=C         LC_TELEPHONE=C       LC_MEASUREMENT=C     LC_IDENTIFICATION=C 

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] data.table_1.12.0

loaded via a namespace (and not attached):
 [1] compiler_3.5.1   assertthat_0.2.0 cli_1.0.1        tools_3.5.1      pillar_1.3.1    
 [6] rstudioapi_0.9.0 tibble_2.0.0     crayon_1.3.4     utf8_1.1.4       fansi_0.4.0     
[11] pkgconfig_2.0.2  rlang_0.3.1   

РЕДАКТИРОВАТЬ:

Спасибо всем за их комментарии.Похоже, что проблема замедления связана с накладными расходами [.data.table, как подробно описано в @Hugh.Та же проблема была упомянута здесь эффективное подмножество data.table с индексами «больше, чем меньше» с использованием , на что указывает @denis.

Исправление, предложенное @Frank, хотя оно действительно эффективно и дает аналогичные результаты, изменяет поведение процесса, полностью удаляя цикл, и добавляет, возможно, нежелательный столбец в исходный набор данных.

EDIT.1:

После моего первого редактирования @Frank добавил еще один подход, который состоит в вычислении столбца списка с использованием синтаксиса data.table.Хотя это довольно опрятно, я должен признать, что мне нужно было время, чтобы понять, что происходит.Я решил, что это просто вычисление lm () в начале и конце столбца подмножества data.table, поэтому я попытался воспроизвести результаты, используя цикл for и data.frames.Время:

> system.time({res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(n = .N, my_lm = list(lm(x.start ~ x.end))), by=.EACHI][, .(n, my_lm)]; res <- as.list(res$my_lm)})
   user  system elapsed 
 11.538   0.003  11.336 
> 
> test_new <- list()
> system.time(
+   for (i in 1:20000) {
+     df_new <- df1[df1$chr == df2$chr[i] & df1$start >= df2$start[i] & df1$end <= df2$end[i],]
+     test_new[[i]] <- lm(df_new$start ~ df_new$end)
+   })
   user  system elapsed 
 12.377   0.048  12.425 
> 

Пока у вас есть функция узкого места как lm (), вам лучше (для контроля и читабельности) использовать базовый цикл for, но использовать data.frames.

Ответы [ 2 ]

0 голосов
/ 30 января 2019

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

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

print(dim(dt1))
#> [1] 4000    3
print(dim(dt2))
#> [1] 20000     3


test1 <- list()
test2 <- list()

bench::system_time({
  for (i in 1:nrow(df2)) {
    no.dim <- dim(df1[df1$chr == df2[i, 'chr'] &
                        df1$start >= df2[i, 'start'] &
                        df1$end <= df2[i, 'end'], ])[1]
    test1[i] <- no.dim
  }
})
#> process    real 
#>  3.547s  3.549s

print(getDTthreads())
#> [1] 12

bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 83.984s 52.266s

setDTthreads(1L)
bench::system_time({
  for (i in 1:nrow(dt2)) {
    no.dim <- dim(dt1[chr == dt2[i, chr] & start >= dt2[i, start] & end <= dt2[i, end], ])[1]
    test2[i] <- no.dim
  }
})
#> process    real 
#> 30.922s 30.920s

Создано в 2019-01-30 представьте пакет (v0.2.1)

Но так же велико, что вы звоните [ 20000 раз.Рассмотрим это минимальное использование, чтобы продемонстрировать, что издержки [.data.table для однорядных таблиц преобладают во время выполнения:

library(data.table)
chr = c(rep(1, 1000), rep(2, 1000), rep(3, 1000), rep(3,1000))
start = rep(seq(from =1, to = 100000, by=100), 4)
end = start + 100

df1 <- data.frame(chr=chr, start=start, end=end)
df2 <- rbind(df1,df1,df1,df1,df1)
dt1 <- data.table::data.table(df1)
dt2 <- data.table::data.table(df2)

bench::system_time({
  o <- integer(nrow(df2))
  for (i in 1:nrow(df2)) {
    o[i] <- df2[i, ][[2]]
  }
})
#>   process      real 
#> 875.000ms 879.398ms

bench::system_time({
  o <- integer(nrow(dt2))
  for (i in 1:nrow(dt2)) {
    o[i] <- dt2[i, ][[2]]
  }
})
#> process    real 
#> 26.219s 13.525s

Создано в 2019-01-30 пакетом представ. (v0.2.1)

0 голосов
/ 29 января 2019

Может ли кто-нибудь воспроизвести такую ​​же ситуацию с замедлением и высокой загрузкой процессора?Можно ли это как-то исправить, сохранив более или менее тот же процесс подмножества, или его нужно полностью переписать, чтобы эффективно использовать в файле data.table?

Я получаю время 5 с и 44 с дляДва подхода OP (DF и DT, соответственно), но ...

system.time(
  dt2[, v := dt1[.SD, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N]
)
#    user  system elapsed 
#    0.03    0.01    0.03 
identical(dt2$v, unlist(test1))
# TRUE

Но допустим, по какой-то причине у вас был скрипт, использующий объекты data.frame, и вы хотели быстроперепишите вещь, чтобы использовать вместо нее data.table.Приведенный выше подход кажется вполне правдоподобным.

Это довольно быстро написать, если вы привыкли к синтаксису data.table.


Если вы не хотитеизменить dt2 просто взять вектор напрямую ...

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .N, by=.EACHI]$N

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

res <- dt1[dt2, on=.(chr, start >= start, end <= end), .(
  n = .N, 
  my_lm = list(lm(x.start ~ x.end))
), by=.EACHI][, .(n, my_lm)]

       n my_lm
    1: 1  <lm>
    2: 1  <lm>
    3: 1  <lm>
    4: 1  <lm>
    5: 1  <lm>
   ---        
19996: 2  <lm>
19997: 2  <lm>
19998: 2  <lm>
19999: 2  <lm>
20000: 2  <lm>
...