`if` 比 ifelse 快吗?

Is `if` faster than ifelse?

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

我们知道 ifelse 很慢 (Does ifelse really calculate both of its vectors every time? Is it slow?),因为它计算所有参数。 `if` 会是一个很好的替代方案吗,因为 if 似乎只评估 TRUE 个参数(这只是我的假设)?


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

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 倍。但是,请注意,这并没有考虑被测试表达式的复杂性和可能的​​优化。

Update:请注意,这个快速基准代表了 ififelse 的一个非常简化且有些偏颇的用例(正如在注释)。虽然它是正确的,但它没有充分代表 ifelse 用例,因为本杰明的回答似乎提供了更公平的比较。

这更多是基于 Roman 的回答的扩展评论,但我需要代码实用程序来说明:

Roman 是正确的,ififelse 快,但我的印象是 if 的速度提升并不是特别有趣,因为它不是什么可以通过矢量化轻松利用。也就是说,仅当 cond/test 参数的长度为 1 时,if 才优于 ifelse

考虑以下函数,它是一个公认的对 if 向量化的弱尝试,没有像 ifelse 那样评估 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 的不太明显*但绝妙的解决方案。

  • by 'less obvious' 我的意思是,我花了两秒钟才意识到他做了什么。根据下面尼古拉的评论,请注意这仅在 yesno 的长度为 1 时有效,否则你会想要坚持使用 ifelse3

是的。我使用 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])
  }

}