I've been studying and writing about grouped operations over the past couple of
months, and one thing that has become apparent to me is that ordering the data
can in at least some circumstances provide a massive performance improvement.
This is primarily due to data.table sharing their radix sort with base R in
3.3.0 which makes it much faster than it used to be.
I think dplyr could take advantage of this pretty easily as evidenced by:
# R3.6.0 w/ dplyr 0.8.1
set.seed(42)
library(dplyr)
library(tibble)
x <- runif(1e7)
grp <- sample(1e6, 1e7, replace=TRUE)
tib <- tibble(grp, x)
system.time(a <- tib %>% group_by(grp) %>% summarise(sum(x)))
## user system elapsed
## 10.369 0.311 10.793
system.time({
o <- order(grp)
tibo <- tibble(grp=grp[o], x=x[o])
b <- tibo %>% group_by(grp) %>% summarise(sum(x))
})
## user system elapsed
## 3.120 0.328 3.474
all.equal(a, b)
## [1] TRUE
The slow step is group_by.
I'm guessing much of the dplyr code was written prior to the data.table radix
sort becoming part of order, so I imagine that may have guided some of the
original design decisions. It seems now that it is an easy win to add a
pre-order to dplyr. There is a penalty for cases where the data is already
ordered, but it is small, and as written above there is some additional memory
usage.
I have not tested this thoroughly, so your mileage may vary across input types.
However the benefit is substantial enough in some cases that it may be worth
spending some time exploring the broader applicability of the change.
I am not familiar with C++, so I have not dug into the sources to narrow down
the problem, but I'm assuming it's a manifestation of similar microarchitectural
factors that affect split that are discussed in this blog post.
This does not appear related to #4334 AFAICT from looking at memory usage via gc():
> library(tibble)
> x <- runif(1e7)
> grp <- sample(1e6, 1e7, replace=TRUE)
> tib <- tibble(grp, x)
> gc()
used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells 491743 26.3 939424 50.2 NA 939424 50.2
Vcells 15868290 121.1 25428491 194.1 16384 15915118 121.5
> system.time(a <- tib %>% group_by(grp) %>% summarise(sum(x)))
user system elapsed
11.301 0.404 11.866
> gc()
used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells 496226 26.6 2478724 132.4 NA 1697148 90.7
Vcells 17377752 132.6 36793026 280.8 16384 25214184 192.4
> system.time({
+ o <- order(grp)
+ tibo <- tibble(grp=grp[o], x=x[o])
+ b <- tibo %>% group_by(grp) %>% summarise(sum(x))
+ })
user system elapsed
3.472 0.270 3.777
> gc()
used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells 496253 26.6 1554928 83.1 NA 1943660 103.9
Vcells 38877692 296.7 67779293 517.2 16384 46702085 356.4
> object.size(tib)
120000984 bytes
I've been studying and writing about grouped operations over the past couple of
months, and one thing that has become apparent to me is that ordering the data
can in at least some circumstances provide a massive performance improvement.
This is primarily due to
data.tablesharing their radix sort with base R in3.3.0 which makes it much faster than it used to be.
I think
dplyrcould take advantage of this pretty easily as evidenced by:The slow step is
group_by.I'm guessing much of the dplyr code was written prior to the
data.tableradixsort becoming part of
order, so I imagine that may have guided some of theoriginal design decisions. It seems now that it is an easy win to add a
pre-order to
dplyr. There is a penalty for cases where the data is alreadyordered, but it is small, and as written above there is some additional memory
usage.
I have not tested this thoroughly, so your mileage may vary across input types.
However the benefit is substantial enough in some cases that it may be worth
spending some time exploring the broader applicability of the change.
I am not familiar with C++, so I have not dug into the sources to narrow down
the problem, but I'm assuming it's a manifestation of similar microarchitectural
factors that affect
splitthat are discussed in this blog post.This does not appear related to #4334 AFAICT from looking at memory usage via
gc():