我认为一般来说使用%>%
不会对速度产生明显影响。但在这种情况下,它运行速度慢了4倍。
library(dplyr)
library(microbenchmark)
set.seed(0)
dummy_data <- dplyr::data_frame(
id=floor(runif(100000, 1, 100000))
, label=floor(runif(100000, 1, 4))
)
microbenchmark(dummy_data %>% group_by(id) %>% summarise(list(unique(label))))
microbenchmark(dummy_data %>% group_by(id) %>% summarise(label %>% unique %>% list))
没有管道:
min lq mean median uq max neval
1.691441 1.739436 1.841157 1.812778 1.880713 2.495853 100
使用烟斗:
min lq mean median uq max neval
6.753999 6.969573 7.167802 7.052744 7.195204 8.833322 100
为什么%>%
在这种情况下会这么慢?有没有更好的方法来写这个?
答案 0 :(得分:30)
在编写与以前“可忽略的”时间相关的单行时,在现实世界的完整应用程序中可能产生的微不足道的影响变得不可忽视。我怀疑如果您对测试进行概要分析,那么大部分时间都在summarize
子句中,所以让microbenchmark类似于:
> set.seed(99);z=sample(10000,4,TRUE)
> microbenchmark(z %>% unique %>% list, list(unique(z)))
Unit: microseconds
expr min lq mean median uq max neval
z %>% unique %>% list 142.617 144.433 148.06515 145.0265 145.969 297.735 100
list(unique(z)) 9.289 9.988 10.85705 10.5820 11.804 12.642 100
这对你的代码做了一些不同的事情,但说明了这一点。管道比较慢。
因为管道需要将R的调用重构为函数评估正在使用的调用,然后对它们进行评估。所以它有更慢。多少取决于功能的快速程度。在{R}中调用unique
和list
非常快,因此这里的全部差异就是管道开销。
这样的分析表达式表明我大部分时间花在管道函数上:
total.time total.pct self.time self.pct
"microbenchmark" 16.84 98.71 1.22 7.15
"%>%" 15.50 90.86 1.22 7.15
"eval" 5.72 33.53 1.18 6.92
"split_chain" 5.60 32.83 1.92 11.25
"lapply" 5.00 29.31 0.62 3.63
"FUN" 4.30 25.21 0.24 1.41
..... stuff .....
然后在大约第15位的某个地方完成了真正的工作:
"as.list" 1.40 8.13 0.66 3.83
"unique" 1.38 8.01 0.88 5.11
"rev" 1.26 7.32 0.90 5.23
然而,如果您只是将这些函数称为Chambers,R直接指向它:
total.time total.pct self.time self.pct
"microbenchmark" 2.30 96.64 1.04 43.70
"unique" 1.12 47.06 0.38 15.97
"unique.default" 0.74 31.09 0.64 26.89
"is.factor" 0.10 4.20 0.10 4.20
因此经常引用的建议是,管道在你的大脑认为是连锁的命令行上是可以的,但不是在可能对时间至关重要的功能中。在实践中,这个开销可能会在一次调用glm
时被几百个数据点消灭,但这是另一个故事....
答案 1 :(得分:3)
但这是我今天学到的东西。我使用的是R 3.5.0。
代码x = 100(1e2)
library(microbenchmark)
library(dplyr)
set.seed(99)
x <- 1e2
z <- sample(x, x / 2, TRUE)
timings <- microbenchmark(
dp = z %>% unique %>% list,
bs = list(unique(z)))
print(timings)
Unit: microseconds
expr min lq mean median uq max neval
dp 99.055 101.025 112.84144 102.7890 109.2165 312.359 100
bs 6.590 7.653 9.94989 8.1625 8.9850 63.790 100
虽然,如果x = 1e6
Unit: milliseconds
expr min lq mean median uq max neval
dp 27.77045 31.78353 35.09774 33.89216 38.26898 52.8760 100
bs 27.85490 31.70471 36.55641 34.75976 39.12192 138.7977 100
答案 2 :(得分:2)
所以,我终于开始在OP的问题中运行表达式了:
set.seed(0)
dummy_data <- dplyr::data_frame(
id=floor(runif(100000, 1, 100000))
, label=floor(runif(100000, 1, 4))
)
microbenchmark(dummy_data %>% group_by(id) %>% summarise(list(unique(label))))
microbenchmark(dummy_data %>% group_by(id) %>% summarise(label %>% unique %>% list))
这花了很长时间以至于我以为我遇到了一个错误,强行打断了R。
再次尝试,重复次数减少,我得到以下时间:
microbenchmark(
b=dummy_data %>% group_by(id) %>% summarise(list(unique(label))),
d=dummy_data %>% group_by(id) %>% summarise(label %>% unique %>% list),
times=2)
#Unit: seconds
# expr min lq mean median uq max neval
# b 2.091957 2.091957 2.162222 2.162222 2.232486 2.232486 2
# d 7.380610 7.380610 7.459041 7.459041 7.537471 7.537471 2
时间是几秒钟!这么多毫秒或微秒。难怪R似乎最初挂了,默认值为times=100
。
但为什么要这么久?首先,构建数据集的方式是id
列包含大约63000个值:
length(unique(dummy_data$id))
#[1] 63052
其次,正在汇总的表达式依次包含多个管道,每组分组数据都会相对较小。
这实际上是管道表达式的最坏情况:它被调用很多次,并且每次都在非常小的输入上运行。这导致了大量的开销,并且没有太多的计算来分摊该开销。
相比之下,如果我们只是切换正在分组和汇总的变量:
microbenchmark(
b=dummy_data %>% group_by(label) %>% summarise(list(unique(id))),
d=dummy_data %>% group_by(label) %>% summarise(id %>% unique %>% list),
times=2)
#Unit: milliseconds
# expr min lq mean median uq max neval
# b 12.00079 12.00079 12.04227 12.04227 12.08375 12.08375 2
# d 10.16612 10.16612 12.68642 12.68642 15.20672 15.20672 2
现在一切看起来都更加平等。
答案 3 :(得分:1)
magrittr 的管道围绕功能链的概念进行编码。
您可以通过以点. %>% head() %>% dim()
开头来创建一个,这是一种编写函数的紧凑方式。
在使用标准管道调用(例如iris %>% head() %>% dim()
)时,仍将首先计算功能链. %>% head() %>% dim()
,从而导致开销。
功能链有点奇怪:
(. %>% head()) %>% dim
#> NULL
当您查看呼叫. %>% head() %>% dim()
时,它实际上解析为`%>%`( `%>%`(., head()), dim())
。基本上,整理出来的东西需要一些操作,这需要一些时间。
需要花费一些时间的另一件事是处理rhs的不同情况,例如iris %>% head
,iris %>% head(.)
,iris %>% {head(.)}
等,在出现错误时在正确的位置插入点相关。
您可以通过以下方式构建非常快的管道:
`%.%` <- function (lhs, rhs) {
rhs_call <- substitute(rhs)
eval(rhs_call, envir = list(. = lhs), enclos = parent.frame())
}
它比magrittr的管道快得多,并且在边缘情况下实际上会表现得更好,但是将需要显式的点并且显然不支持功能链。
library(magrittr)
`%.%` <- function (lhs, rhs) {
rhs_call <- substitute(rhs)
eval(rhs_call, envir = list(. = lhs), enclos = parent.frame())
}
bench::mark(relative = T,
"%>%" =
1 %>% identity %>% identity() %>% (identity) %>% {identity(.)},
"%.%" =
1 %.% identity(.) %.% identity(.) %.% identity(.) %.% identity(.)
)
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 %>% 15.9 13.3 1 4.75 1
#> 2 %.% 1 1 17.0 1 1.60
由reprex package(v0.3.0)于2019-10-05创建
这里的时钟速度是以前的13倍。
我将其包含在名为%>>%
的实验性fastpipe程序包中。
现在,我们还可以通过简单的更改直接利用功能链的功能:
dummy_data %>% group_by(id) %>% summarise_at('label', . %>% unique %>% list)
这将更快,因为功能链仅被解析了一次,然后在内部它只是一个循环接一个地应用函数,非常接近您的基本解决方案。另一方面,由于对每个循环实例和每个管道都进行了评估/替换,因此我的快速管道仍会增加少量开销。
这里有一个基准,包括这两个新解决方案:
microbenchmark::microbenchmark(
nopipe=dummy_data %>% group_by(id) %>% summarise(label = list(unique(label))),
magrittr=dummy_data %>% group_by(id) %>% summarise(label = label %>% unique %>% list),
functional_chain=dummy_data %>% group_by(id) %>% summarise_at('label', . %>% unique %>% list),
fastpipe=dummy_data %.% group_by(., id) %.% summarise(., label =label %.% unique(.) %.% list(.)),
times = 10
)
#> Unit: milliseconds
#> expr min lq mean median uq max neval cld
#> nopipe 42.2388 42.9189 58.0272 56.34325 66.1304 80.5491 10 a
#> magrittr 512.5352 571.9309 625.5392 616.60310 670.3800 811.1078 10 b
#> functional_chain 64.3320 78.1957 101.0012 99.73850 126.6302 148.7871 10 a
#> fastpipe 66.0634 87.0410 101.9038 98.16985 112.7027 172.1843 10 a