Tidyr: nest() / unnest() em 1.0.0 significativamente mais lento

Criado em 18 set. 2019  ·  9Comentários  ·  Fonte: tidyverse/tidyr

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
feature performance rectangling vctrs ↗️

Comentários muito úteis

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.

Todos 9 comentários

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.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

jennybc picture jennybc  ·  5Comentários

strengejacke picture strengejacke  ·  8Comentários

romagnolid picture romagnolid  ·  8Comentários

atusy picture atusy  ·  4Comentários

coatless picture coatless  ·  6Comentários