if语句比ifelse语句更快吗?

18

最近在重新阅读Hadley的Advanced R时,我注意到他在第6章中说`if`可以像函数一样使用,例如:`if`(i == 1, print("yes"), print("no"))(如果你手头有实体书,它在第80页)

我们知道ifelse很慢(ifelse是否真的每次都计算其两个向量?速度很慢吗?),因为它评估所有参数。由于if似乎只评估TRUE参数(这只是我的假设),那么`if`是否是一个好的替代品呢?


更新:根据@Benjamin和@Roman的答案以及@Gregor和许多其他人的评论,ifelse似乎是矢量化计算的更好解决方案。我选择@Benjamin的答案,因为它提供了更全面的比较,对社区的福祉更有益。但是,两个答案(以及评论)都值得一读。


4
如果只针对一个元素起作用... - Heroka
7
ififelse不是很好的比较,for (i in 1:n) { if(condition[i]){...} else {...} }ifelse 则可进行比较。 - Gregor Thomas
3个回答

18

这更像是对Roman答案的延伸评论,但我需要代码实用工具来详细阐述:

Roman正确指出ififelse更快,但我认为if的速度提升并不特别有趣,因为它不能轻易地通过向量化来利用。也就是说,只有在cond/test参数的长度为1时,if才比ifelse优越。

考虑下面的函数,这是一种尝试将if向量化但没有评估yesno条件的副作用的薄弱方法。

ifelse2 <- function(test, yes, no){
 result <- rep(NA, length(test))
 for (i in seq_along(test)){
   result[i] <- `if`(test[i], yes[i], no[i])
 }
 result
}

ifelse2a <- function(test, yes, no){
  sapply(seq_along(test),
         function(i) `if`(test[i], yes[i], no[i]))
}

ifelse3 <- function(test, yes, no){
  result <- rep(NA, length(test))
  logic <- test
  result[logic] <- yes[logic]
  result[!logic] <- no[!logic]
  result
}


set.seed(pi)
x <- rnorm(1000)

library(microbenchmark)
microbenchmark(
  standard = ifelse(x < 0, x^2, x),
  modified = ifelse2(x < 0, x^2, x),
  modified_apply = ifelse2a(x < 0, x^2, x),
  third = ifelse3(x < 0, x^2, x),
  fourth = c(x, x^2)[1L + ( x < 0 )],
  fourth_modified = c(x, x^2)[seq_along(x) + length(x) * (x < 0)]
)

Unit: microseconds
            expr     min      lq      mean  median       uq      max neval cld
        standard  52.198  56.011  97.54633  58.357  68.7675 1707.291   100 ab 
        modified  91.787  93.254 131.34023  94.133  98.3850 3601.967   100  b 
  modified_apply 645.146 653.797 718.20309 661.568 676.0840 3703.138   100   c
           third  20.528  22.873  76.29753  25.513  27.4190 3294.350   100 ab 
          fourth  15.249  16.129  19.10237  16.715  20.9675   43.695   100 a  
 fourth_modified  19.061  19.941  22.66834  20.528  22.4335   40.468   100 a 

一些编辑: 感谢Frank和Richard Scriven指出我的不足。

正如你所见,将向量分解以适合传递给if的过程是一个耗时的过程,最终比只运行ifelse要慢(这可能就是为什么没有人费心实现我的解决方案)。

如果你真的急需提高速度,你可以使用上面的ifelse3方法。或者更好的选择是Frank不太明显*但很棒的解决方案。

  • 所谓“不太明显”,我的意思是,我花了两秒钟才意识到他做了什么。根据nicola下面的评论,请注意,此方法仅在yesno长度为1时有效,否则您应该坚持使用ifelse3

2
嗯,fourth = c("非负", "负")[1L + ( x < 0 )] - Frank
1
我选择简单而不是优雅,尽管(简单是主观的)。但既然你要求了,我会将它添加进去。但与基本的 ifelse 相比仍运行得更慢。 - Benjamin
2
@RomanTsegelskyi 如果你认为 "forsapply 慢得多",那意味着你还没有充分熟悉 R 的使用。 - nicola
2
在许多情况下,sapplyfor 循环慢,因为它调用了 simplify2array。另一方面,lapplyvapply 通常比循环更快。没有任何一种 *apply 比循环快得多,尽管许多人认为是这样的。 - nicola
1
@Benjamin 第四个解决方案不起作用。你知道自己在做什么吗?当“yes”和“no”长度为一时,该解决方案才有效。 - nicola
显示剩余7条评论

10

if 是通过 .Primitive 接口调用的原始(编译的)函数,而 ifelse 是 R 字节码,因此似乎 if 会更快。运行一些快速基准测试。

> microbenchmark(`if`(TRUE, "a", "b"), ifelse(TRUE, "a", "b"))
Unit: nanoseconds
                   expr  min   lq    mean median     uq   max neval cld
 if (TRUE) "a" else "b"   46   54  372.59   60.0   68.0 30007   100  a 
 ifelse(TRUE, "a", "b") 1212 1327 1581.62 1442.5 1617.5 11743   100   b

> microbenchmark(`if`(FALSE, "a", "b"), ifelse(FALSE, "a", "b"))
Unit: nanoseconds
                    expr  min   lq    mean median   uq   max neval cld
 if (FALSE) "a" else "b"   47   55   91.64   61.5   73  2550   100  a 
 ifelse(FALSE, "a", "b") 1256 1346 1688.78 1460.0 1677 17260   100   b

看起来,不考虑实际分支中的代码,if至少比ifelse快20倍。但需要注意的是,这并没有考虑测试表达式的复杂性以及可能存在的优化。

更新:请注意,这个快速基准测试表示了ififelse的一个非常简单和有些偏见的使用情况(正如评论中所指出的)。虽然它是正确的,但它低估了ifelse的使用情况,对此,Benjamin的答案似乎提供了更公平的比较。


1
好的,那么你已经节省了不到一秒钟的时间...在什么情况下这可能有用呢?大家应该使用适合工作的正确工具。 - Frank
4
@Frank,为什么一切都必须有用呢?只是抱着好奇心不行吗? :) - romants
4
好奇心害死猫。 - Rich Scriven
6
好奇心是好的,但危险在于R初学者读到这篇文章后会认为:“我应该总是使用if而不是ifelse,因为它更快”,而不理解使用情况的差异。 - Gregor Thomas
4
当仅涉及单个条件时,使用if比使用ifelse更快,使用+-比使用sumdiff更快,这似乎毫不奇怪。 - Gregor Thomas
显示剩余3条评论

0

是的。我使用ifelse()开发了一个处理152589条记录的程序,用时90分钟,但使用if()改进后只需要25分钟。

for(i in ...){
  # "Case 1"
  # asesorMinimo<-( dummyAsesor%>%filter(FechaAsignacion==min(FechaAsignacion)) )[1,] 
  # asesorRegla<-tail(dummyAsesor%>%filter( FechaAsignacion<=dumFinClase)%>%arrange(FechaAsignacion),1)
  # #Asigna Asesor
  # dummyRow<-dummyRow%>%mutate(asesorRetencion=ifelse(dim(asesorRegla)[1]==0,asesorMinimo$OperadorNombreApellido,asesorRegla$OperadorNombreApellido))



  # "Case 2"
  asesorRegla<-tail(dummyAsesor%>%filter( FechaAsignacion<=dumFinClase)%>%arrange(FechaAsignacion),1)
  asesorMinimo<-( dummyAsesor%>%filter(FechaAsignacion==min(FechaAsignacion)) )[1,] 
  if(dim(asesorRegla)[1]==0){
    dummyRow<-dummyRow%>%mutate(asesorRetencion=asesorMinimo[1,7])
  }else{
    dummyRow<-dummyRow%>%mutate(asesorRetencion=asesorRegla[1,7])
  }

}

欢迎来到StackOverflow!你的回答是基于经验事实的陈述。我猜测OP已经知晓这个事实,需要的是建议或解释。这个问题有很好的解答和解释。如果你认为你可以给出更好的答案,请提供额外的细节。 - Maxim Sagaydachny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接