Parece que a nova implementação de nest()
e unnest()
resultou em uma desaceleração dramática, em comparação com a versão anterior de tidyR
Talvez o problema esteja relacionado à pré-alocação de tamanho?
Nesse caso, num_rows
precisa ser grande para observar a desaceleração. Veja trecho de código abaixo.
num_rows <- 100000
x <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1))
before <- Sys.time()
y <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)) %>%
tidyr::nest(second_and_third = c(second, third)) %>%
tidyr::unnest(second_and_third)
after <- Sys.time()
if(length(which(x != y)) != 0){
stop("nest() and unnest() procedure results in corrupted data!")
}
cat(paste("Execution Time:",difftime(after,before,units="secs"),"seconds"))
No meu sistema:
Execution Time: 61.2449209690094 seconds
Eu testei isso com cleanR 0.8.3
:
num_rows <- 100000
x <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1))
before <- Sys.time()
y <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)) %>%
tidyr::nest(second_and_third = c(second, third)) %>%
tidyr::unnest()
after <- Sys.time()
if(length(which(x != y)) != 0){
stop("nest() and unnest() procedure results in corrupted data!")
}
cat(paste("Execution Time:",difftime(after,before,units="secs"),"seconds"))
e aí eu vi
Execution Time: 5.78480935096741 seconds
Desaceleração de 10,587x
Vale ressaltar que nest_legacy()
e unnest_legacy()
ainda são rápidos:
num_rows <- 100000
x <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1))
before <- Sys.time()
y <- dplyr::tibble(first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)) %>%
tidyr::nest_legacy(second_and_third = c(second, third)) %>%
tidyr::unnest_legacy()
after <- Sys.time()
if(length(which(x != y)) != 0){
stop("nest() and unnest() procedure results in corrupted data!")
}
cat(paste("Execution Time:",difftime(after,before,units="secs"),"seconds"))
Na minha minha máquina
Execution Time: 3.19051384925842 seconds
Assim, para código performático, sempre se pode usar as funções legacy()
. Eu sugeriria corrigir isso para a próxima versão principal e direcionar as pessoas a usar as funções legacy()
para evitar lentidão inesperadas.
Eu também comentários não são muito úteis. Por favor, basta clicar no botão de polegar para cima.
A maioria dos problemas aqui foram resolvidos na versão de desenvolvimento do vctrs. O problema foi principalmente com unnest()
(realmente, unchop()
). Consulte r-lib/vctrs#530 para obter mais informações.
Com seu exemplo exato:
# devtools::install_github("r-lib/vctrs")
library(tidyr)
num_rows <- 100000
x <- dplyr::tibble(
first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)
)
before <- Sys.time()
y <- dplyr::tibble(
first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)
) %>%
tidyr::nest(second_and_third = c(second, third)) %>%
tidyr::unnest(second_and_third)
after <- Sys.time()
if(length(which(x != y)) != 0){
stop("nest() and unnest() procedure results in corrupted data!")
}
cat(paste("Execution Time:",difftime(after,before,units="secs"),"seconds"))
#> Execution Time: 9.04665207862854 seconds
Criado em 24/09/2019 pelo pacote reprex (v0.2.1)
Acho que poderíamos ficar 1-2 segundos mais rápidos com r-lib/vctrs#592.
E provavelmente um pouco mais de uma implementação nativa C de vec_recycle_common()
.
Reprex comparando funções novas e legadas diretamente:
library(tidyr)
num_rows <- 10000
df <- tibble(
first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)
)
bench::mark(
new = df %>%
nest(second_and_third = c(second, third)) %>%
unnest(second_and_third),
old = df %>%
nest_legacy(second, third) %>%
unnest_legacy(data)
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 new 906ms 906ms 1.10 3.6MB 27.6
#> 2 old 384ms 404ms 2.48 2.98MB 24.8
Criado em 13/11/2019 pelo pacote reprex (v0.3.0)
Portanto, o pior da lacuna de desempenho agora está resolvido, embora obviamente seja melhor fazer melhor do que a versão anterior (embora a nova versão seja muito mais geral, portanto não é muito surpreendente que seja um pouco mais lenta atualmente). A criação de perfil mostra ~15% do tempo de execução de drop_null()
e 70% de vec_rbind()
, então, como sugere @DavisVaughan , vctrs é o lugar óbvio para buscar melhorias de desempenho .
Em teoria, aqui estão os benefícios de r-lib/vctrs#592
library(tidyr)
num_rows <- 10000
tbl <- tibble(
first = 1:num_rows,
second = 5:(num_rows+5-1),
third = 7:(num_rows+7-1)
)
tbl_nest <- tbl %>%
nest(second_and_third = c(second, third))
df_nest <- as.data.frame(tbl_nest)
df_nest$second_and_third <- lapply(df_nest$second_and_third, as.data.frame)
bench::mark(
tibble_new = unnest(tbl_nest, second_and_third),
tibble_old = unnest_legacy(tbl_nest, second_and_third),
dataframe_new = unnest(df_nest, second_and_third),
dataframe_old = unnest_legacy(df_nest, second_and_third),
iterations = 30
)
#> Warning: Some expressions had a GC in every iteration; so filtering is
#> disabled.
#> # A tibble: 4 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 tibble_new 622.2ms 698ms 1.37 1.43MB 22.7
#> 2 tibble_old 92.4ms 98.9ms 10.1 808.02KB 23.2
#> 3 dataframe_new 345.6ms 431.9ms 2.25 680.05KB 19.4
#> 4 dataframe_old 62.3ms 67.2ms 14.7 575.67KB 28.3
Criado em 13/11/2019 pelo pacote reprex (v0.3.0.9000)
Reprex um pouco mais simples:
library(tidyr)
n <- 10000
df <- tibble(
g = 1:n,
y = rep(list(tibble(x = 1:5)), n)
)
bench::mark(
unnest(df, y),
unnest_legacy(df, y)
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 unnest(df, y) 488.4ms 512.8ms 1.95 2.82MB 22.4
#> 2 unnest_legacy(df, y) 88.6ms 91.9ms 10.7 1.44MB 24.9
Criado em 28/11/2019 pelo pacote reprex (v0.3.0)
Atualização incremental. Com vctrs master branch após a inclusão de grandes benefícios de r-lib/vctrs#825 e pequenos benefícios de r-lib/vctrs#824
library(tidyr)
n <- 10000
df <- tibble(
g = 1:n,
y = rep(list(tibble(x = 1:5)), n)
)
bench::mark(
unnest(df, y),
unnest_legacy(df, y),
iterations = 50
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 unnest(df, y) 204ms 297.5ms 3.39 3.38MB 14.0
#> 2 unnest_legacy(df, y) 48ms 73.6ms 13.4 1.25MB 11.6
Criado em 17/02/2020 pelo pacote reprex (v0.3.0)
Eu tenho outra ideia para reduzir ainda mais isso movendo alguns detalhes de implementação tidyr::unchop()
caros para C
Com o dev dplyr, estou vendo agora:
library(tidyr)
n <- 10000
df <- tibble(
g = 1:n,
y = rep(list(tibble(x = 1:5)), n)
)
bench::mark(
unnest(df, y),
unnest_legacy(df, y)
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 unnest(df, y) 87.2ms 90.8ms 11.0 3.62MB 20.1
#> 2 unnest_legacy(df, y) 123.7ms 129.6ms 7.73 4.11MB 23.2
Criado em 22-04-2020 pelo pacote reprex (v0.3.0)
Então unnest_legacy()
diminuiu um pouco, mas unnest()
agora é mais rápido, e apenas um pouco mais lento do que antes.
Acho que este é um bom lugar para deixá-lo. Certamente podemos voltar para melhorar o desempenho no futuro, mas acho que a motivação original está resolvida.
Comentários muito úteis
Vale ressaltar que
nest_legacy()
eunnest_legacy()
ainda são rápidos:Na minha minha máquina
Assim, para código performático, sempre se pode usar as funções
legacy()
. Eu sugeriria corrigir isso para a próxima versão principal e direcionar as pessoas a usar as funçõeslegacy()
para evitar lentidão inesperadas.