lapply vs for 循环 - 性能 R
Posted
技术标签:
【中文标题】lapply vs for 循环 - 性能 R【英文标题】:lapply vs for loop - Performance R 【发布时间】:2017-07-12 15:07:44 【问题描述】:人们常说应该更喜欢lapply
而不是for
循环。
有一些例外,例如 Hadley Wickham 在他的 Advance R 书中指出。
(http://adv-r.had.co.nz/Functionals.html)(就地修改、递归等)。 以下是这种情况之一。
只是为了学习,我尝试以函数形式重写感知器算法以进行基准测试 相对表现。 来源 (https://rpubs.com/FaiHas/197581)。
这里是代码。
# prepare input
data(iris)
irissubdf <- iris[1:100, c(1, 3, 5)]
names(irissubdf) <- c("sepal", "petal", "species")
head(irissubdf)
irissubdf$y <- 1
irissubdf[irissubdf[, 3] == "setosa", 4] <- -1
x <- irissubdf[, c(1, 2)]
y <- irissubdf[, 4]
# perceptron function with for
perceptron <- function(x, y, eta, niter)
# initialize weight vector
weight <- rep(0, dim(x)[2] + 1)
errors <- rep(0, niter)
# loop over number of epochs niter
for (jj in 1:niter)
# loop through training data set
for (ii in 1:length(y))
# Predict binary label using Heaviside activation
# function
z <- sum(weight[2:length(weight)] * as.numeric(x[ii,
])) + weight[1]
if (z < 0)
ypred <- -1
else
ypred <- 1
# Change weight - the formula doesn't do anything
# if the predicted value is correct
weightdiff <- eta * (y[ii] - ypred) * c(1,
as.numeric(x[ii, ]))
weight <- weight + weightdiff
# Update error function
if ((y[ii] - ypred) != 0)
errors[jj] <- errors[jj] + 1
# weight to decide between the two species
return(errors)
err <- perceptron(x, y, 1, 10)
### my rewriting in functional form auxiliary
### function
faux <- function(x, weight, y, eta)
err <- 0
z <- sum(weight[2:length(weight)] * as.numeric(x)) +
weight[1]
if (z < 0)
ypred <- -1
else
ypred <- 1
# Change weight - the formula doesn't do anything
# if the predicted value is correct
weightdiff <- eta * (y - ypred) * c(1, as.numeric(x))
weight <<- weight + weightdiff
# Update error function
if ((y - ypred) != 0)
err <- 1
err
weight <- rep(0, 3)
weightdiff <- rep(0, 3)
f <- function()
t <- replicate(10, sum(unlist(lapply(seq_along(irissubdf$y),
function(i)
faux(irissubdf[i, 1:2], weight, irissubdf$y[i],
1)
))))
weight <<- rep(0, 3)
t
由于上述原因,我没想到会有任何持续的改进
问题。但是,当我看到急剧恶化时,我真的很惊讶
使用lapply
和replicate
。
我使用 microbenchmark
库中的 microbenchmark
函数获得了这个结果
可能的原因是什么? 会不会是内存泄露?
expr min lq mean median uq
f() 48670.878 50600.7200 52767.6871 51746.2530 53541.2440
perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) 4184.131 4437.2990 4686.7506 4532.6655 4751.4795
perceptronC(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) 95.793 104.2045 123.7735 116.6065 140.5545
max neval
109715.673 100
6513.684 100
264.858 100
第一个函数是lapply
/replicate
函数
第二个是带有for
循环的函数
第三个是C++
中同样的函数使用Rcpp
这里根据 Roland 的功能剖析。 我不确定我能否以正确的方式解释它。 在我看来,大部分时间都花在子集上 Function profiling
【问题讨论】:
请准确。我在您的函数f
中没有看到对 apply
的任何调用。
我建议你学习如何分析函数:adv-r.had.co.nz/Profiling.html
您的代码中有几个错误;首先,irissubdf[, 4] <- 1
应该是irissubdf$y <- 1
,所以以后可以使用该名称,其次,weight
在f
中使用之前没有定义。我也不清楚<<-
在你的lapply
和replicate
命令中做正确的事情,但我不清楚它应该做什么。这也可能是两者之间的主要区别; <<-
必须处理环境,而另一个则不需要,虽然我不知道这可能会产生什么影响,但它不再是苹果与苹果之间的比较了。
感谢指出,我只是忘记复制初始化权重(和权重差异)的代码。我使用了
嗨,我出于好奇尝试删除
【参考方案1】:
首先,for
循环比lapply
慢是一个早已被揭穿的神话。 R 中的 for
循环已经提高了很多性能,目前至少与 lapply
一样快。
也就是说,您必须在此处重新考虑您对lapply
的使用。您的实现需要分配给全局环境,因为您的代码需要您在循环期间更新权重。这是不考虑lapply
的正当理由。
lapply
是一个你应该使用它的副作用(或没有副作用)的函数。与for
循环相反,lapply
函数会自动将结果组合到一个列表中,并且不会影响您工作的环境。 replicate
也是如此。另请参阅此问题:
Is R's apply family more than syntactic sugar?
lapply
解决方案慢得多的原因是您使用它的方式会产生更多开销。
replicate
在内部只不过是 sapply
,因此您实际上结合了 sapply
和 lapply
来实现双循环。 sapply
会产生额外的开销,因为它必须测试结果是否可以简化。所以for
循环实际上会比使用replicate
更快。
在您的lapply
匿名函数中,您必须为每次观察访问x 和y 的数据框。这意味着 - 与你的 for 循环相反 - 例如函数 $
每次都必须被调用。
因为您使用这些高端函数,所以您的“lapply”解决方案调用了 49 个函数,而您的 for
解决方案只调用了 26 个函数。lapply
解决方案的这些额外函数包括对 match
等函数的调用, structure
, [[
, names
, %in%
, sys.call
, duplicated
, ...
for
循环不需要的所有函数,因为它不执行任何这些检查。
如果您想了解这些额外开销来自何处,请查看replicate
、unlist
、sapply
和simplify2array
的内部代码。
您可以使用以下代码更好地了解您在使用lapply
时失去性能的地方。逐行运行!
Rprof(interval = 0.0001)
f()
Rprof(NULL)
fprof <- summaryRprof()$by.self
Rprof(interval = 0.0001)
perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10)
Rprof(NULL)
perprof <- summaryRprof()$by.self
fprof$Fun <- rownames(fprof)
perprof$Fun <- rownames(perprof)
Selftime <- merge(fprof, perprof,
all = TRUE,
by = 'Fun',
suffixes = c(".lapply",".for"))
sum(!is.na(Selftime$self.time.lapply))
sum(!is.na(Selftime$self.time.for))
Selftime[order(Selftime$self.time.lapply, decreasing = TRUE),
c("Fun","self.time.lapply","self.time.for")]
Selftime[is.na(Selftime$self.time.for),]
【讨论】:
我对此答案中声称的揭穿的任何参考资料都非常感兴趣。你能在这里提供一些吗?【参考方案2】:还有更多关于何时使用for
或lapply
以及哪个“性能”更好的问题。有时速度很重要,有时内存很重要。更复杂的是,时间复杂度可能不是您所期望的 - 也就是说,可以在不同的范围内观察到不同的行为,从而使任何笼统的陈述无效,例如“比”或“至少和一样快”。最后,一个经常被忽视的性能指标是thought-to-code,未成熟的优化 yada yada。
也就是说,在Introduction to R 中,作者暗示了一些性能问题:
警告:R 代码中使用 for() 循环的频率远低于编译语言。采用“整个对象”视图的代码在 R 中可能会更清晰、更快速。
给定一个类似的用例,输入和输出,不考虑用户偏好,明显优于另一个?
基准 - 斐波那契数列
我将计算 1 的方法与 N Fibonacci numbers(受 benchmarkme 包的启发)进行比较,避开 2nd Circle 并确保每种方法的输入和输出相同。还包括四种额外的方法来火上浇油——矢量化方法和purrr::map
,以及*apply
变体vapply
和sapply
。
fib <- function(x, ...)
x <- 1:x ; phi = 1.6180339887498949 ; v = \() vector("integer", length(x))
bench::mark(
vector =
y=v(); y = ((rep(phi, length(x))^x) - ((-rep(phi, length(x)))^-x)) / sqrt(5); y,
lapply =
y=v(); y = unlist(lapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5)), use.names = F); y,
loop =
y=v(); `for`(i, x, y[i] = (phi^i - (-phi)^(-i)) / sqrt(5)); y,
sapply =
y=v(); y = sapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5)); y,
vapply =
y=v(); y = vapply(x, \(.) (phi^. - (-phi)^(-.)) / sqrt(5), 1); y,
map =
y=v(); y <- purrr::map_dbl(x, ~ (phi^. - (-phi)^(-.))/sqrt(5)); y
, ..., check = T
)[c(1:9)]
这是性能比较,按中位时间排名。
lapply(list(3e2, 3e3, 3e4, 3e5, 3e6, 3e7), fib) # n iterations specified separately
N = 300
expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time
1 vector 38.8us 40.9us 21812. 8.44KB 0 1000 0 45.8ms
2 vapply 500us 545us 1653. 3.61KB 1.65 999 1 604ms
3 sapply 518us 556us 1725. 12.48KB 0 1000 0 580ms
4 lapply 513.4us 612.8us 1620. 6KB 8.14 995 5 614.2ms
5 loop 549.9us 633.6us 1455. 3.61KB 8.78 994 6 683.3ms
6 map 649.6us 754.6us 1312. 3.61KB 9.25 993 7 756.9ms
N = 3000
1 vector 769.7us 781.5us 1257. 82.3KB 1.26 999 1 794.83ms
2 vapply 5.38ms 5.58ms 173. 35.2KB 0.697 996 4 5.74s
3 sapply 5.59ms 5.83ms 166. 114.3KB 0.666 996 4 6.01s
4 loop 5.38ms 5.91ms 167. 35.2KB 8.78 950 50 5.69s
5 lapply 5.24ms 6.49ms 156. 58.7KB 8.73 947 53 6.07s
6 map 6.11ms 6.63ms 148. 35.2KB 9.13 942 58 6.35s
N = 30 000
1 vector 10.7ms 10.9ms 90.9 821KB 0.918 297 3 3.27s
2 vapply 57.3ms 60.1ms 16.4 351.66KB 0.741 287 13 17.5s
3 loop 59.2ms 60.7ms 15.9 352KB 16.7 146 154 9.21s
4 sapply 59.6ms 62.1ms 15.7 1.05MB 0.713 287 13 18.2s
5 lapply 57.3ms 67.6ms 15.1 586KB 20.5 127 173 8.43s
6 map 66.7ms 69.1ms 14.4 352KB 21.6 120 180 8.35s
N = 300 000
1 vector 190ms 193ms 5.14 8.01MB 0.206 100 4 19.45s
2 loop 693ms 713ms 1.40 3.43MB 7.43 100 532 1.19m
3 map 766ms 790ms 1.26 3.43MB 7.53 100 598 1.32m
4 vapply 633ms 814ms 1.33 3.43MB 0.851 100 39 45.8s
5 lapply 685ms 966ms 1.06 5.72MB 9.13 100 864 1.58m
6 sapply 694ms 813ms 1.27 12.01MB 0.810 100 39 48.1s
N = 3 000 000
1 vector 3.17s 3.21s 0.312 80.1MB 0.249 20 16 1.07m
2 vapply 8.22s 8.37s 0.118 34.3MB 4.97 20 845 2.83m
3 loop 8.3s 8.42s 0.119 34.3MB 4.35 20 733 2.81m
4 map 9.09s 9.17s 0.109 34.3MB 4.91 20 903 3.07m
5 lapply 10.42s 11.09s 0.0901 57.2MB 4.10 20 909 3.7m
6 sapply 10.43s 11.28s 0.0862 112.1MB 3.58 20 830 3.87m
N = 30 000 000
1 vector 44.8s 45.94s 0.0214 801MB 0.00854 10 4 7.8m
2 vapply 1.56m 1.6m 0.0104 343MB 0.883 10 850 16m
3 loop 1.56m 1.62m 0.00977 343MB 0.366 10 374 17.1m
4 map 1.72m 1.74m 0.00959 343MB 1.23 10 1279 17.4m
5 lapply 2.15m 2.22m 0.00748 572MB 0.422 10 565 22.3m
6 sapply 2.05m 2.25m 0.00747 1.03GB 0.405 10 542 22.3m
# Intel i5-8300H CPU @ 2.30GHz / R version 4.1.1 / purrr 0.3.4
for
和lapply
方法的性能相似,但lapply
在内存方面更贪婪,并且在输入大小增加时会慢一些(对于此任务)。请注意,purrr::map
的内存使用量等同于for-loop
,优于lapply
,本身a debated topic。但是,当使用适当的*apply*
,这里vapply
,性能是相似的。但该选择可能会对内存使用产生很大影响,sapply
的内存效率明显低于vapply
。
深入了解这些方法的不同性能的原因。 for-loop 执行许多类型检查,导致一些开销。另一方面,lapply 遭受flawed 语言设计的困扰,其中延迟评估或使用承诺是有代价的,源代码确认X
和FUN
的参数.Internal(lapply)
是承诺.
矢量化方法很快,并且可能比for
或lapply
方法更可取。请注意,与其他方法相比,矢量化方法是如何不规则地增长的。但是,矢量化代码的美观性可能是一个问题:您更喜欢调试哪种方法?
总的来说,我想说lapply
或for
之间的选择不是普通R 用户应该考虑的。坚持最容易编写、思考和调试的东西,或者更不容易(沉默?)出错的东西。性能上的损失可能会被节省的写入时间抵消。对于性能关键的应用程序,请确保使用不同的输入大小运行一些测试并正确分块代码。
【讨论】:
【参考方案3】:其实
我确实用最近解决的一个问题测试了差异。
自己试试吧。
在我的结论中,没有区别,但我的情况下 for 循环比 lapply 快得多。
Ps:我尽量保持使用相同的逻辑。
ds <- data.frame(matrix(rnorm(1000000), ncol = 8))
n <- c('a','b','c','d','e','f','g','h')
func <- function(ds, target_col, query_col, value)
return (unique(as.vector(ds[ds[query_col] == value, target_col])))
f1 <- function(x, y)
named_list <- list()
for (i in y)
named_list[[i]] <- func(x, 'a', 'b', i)
return (named_list)
f2 <- function(x, y)
list2 <- lapply(setNames(nm = y), func, ds = x, target_col = "a", query_col = "b")
return(list2)
benchmark(f1(ds2, n ))
benchmark(f2(ds2, n ))
如您所见,我做了一个简单的例程来构建基于数据帧的 named_list,func 函数执行提取的列值,f1 使用 for 循环遍历数据帧,f2 使用 lapply 函数。
在我的电脑上我得到这个结果:
test replications elapsed relative user.self sys.self user.child
1 f1(ds2, n) 100 110.24 1 110.112 0 0
sys.child
1 0
&&
test replications elapsed relative user.self sys.self user.child
1 f1(ds2, n) 100 110.24 1 110.112 0 0
sys.child
1 0
【讨论】:
您的脚本不是独立的。您能否为benchmark()
函数指定library()
并同时定义ds2
?
你的输出是f1
的两倍以上是关于lapply vs for 循环 - 性能 R的主要内容,如果未能解决你的问题,请参考以下文章
使用lapply或for循环将多个csv文件拉入自己的R数据帧
对具有相同结构的几个数据集使用lapply并可能进行for循环以提取和计算每个数据帧的值