R:使用 class 属性对列表元素的赋值较慢

R: Assignments to list elements slower with class attribute

我在 S3 和 S4 classes 的上下文中做了一些分析并观察到以下内容:

与相应普通列表上的相同操作相比,对 S3 对象的元素进行简单的赋值大约慢 2-3 倍。

从我的角度来看,S3 class 是一个带有附加属性的列表,元素只是一个数字。那么,哪些机制消耗了额外的时间?

value <- 1 
obj_list <- list( a = 0 )
obj_s3 <- structure( obj_list, class = "myclass" )

system.time( 
  replicate( 100000, obj_list$a <- value)
) # ~180 ms

system.time( 
  replicate( 100000, obj_s3$a <- value)
) # ~420 ms

只要您将 class 添加到您的 R 变量,您就可以将其设为受 S3 调度约束的对象。由于 $<- 的行为类似于 S3 泛型,$<- 将尝试根据对象的 class 进行分派。如果查看 $<- 的 C 代码,您可以看到:

/* From src/main/subassign.c

   $<-(x, elt, val)
*/
SEXP attribute_hidden do_subassign3(SEXP call, SEXP op, SEXP args, SEXP env)
{
  // ... code omitted  
  if(DispatchOrEval(call, op, "$<-", args, env, &ans, 0, 0))
      return(ans);
  // ... code omitted  
}

DispatchOrEval 只会在参数是一个对象(即有一个 class 或者是一个 S4 对象)时启动 S3 分派。请注意,即使对于没有方法的对象,S3 dispatch 也会有开销,因为仍然必须找到默认方法。如果我们查看像 mean 这样的非原始 S3 泛型,这会更清楚一些,其中分派过程很明显是:

> mean
function (x, ...) 
UseMethod("mean")
<bytecode: 0x000000000fd151c0>
<environment: namespace:base>

这表明当您在没有方法的情况下对对象调用 mean 时,确实发生了以下情况:

mean(obj) => UseMethod() => find method => mean.default(obj)

额外的调用和查找匹配方法的过程增加了您观察到的开销。这对于 $<-sum 之类的东西并不明显,因为所有这些东西都是通过 DispatchOrEval.

中的 C 代码完成的

举例说明:

> obj <- structure(1:10, class="wookkawooka")
> var <- 1:10
> 
> library(microbenchmark)
> microbenchmark(mean(obj), mean(var), mean.default(obj))
Unit: microseconds
              expr    min     lq     mean median      uq    max neval
         mean(obj) 12.069 13.166 16.46442 13.166 13.7145 95.813   100
         mean(var)  8.046  8.777  9.51974  8.778  9.1430 31.084   100
 mean.default(obj)  6.217  7.314  9.17234  7.680  8.0460 84.111   100

请注意,此处并未显示差异,因为 mean.default 函数本身比 $<- 等原语具有更多的开销,因此调度时间在总时间中所占的比例较小。另外,请注意,对于非对象,调度仍然会发生(与原语不同),除了可以更快地做出使用默认方法的决定。这就是 mean(var)mean.default(obj) 慢一点但比 mean(obj) 快的原因。

这是您可能感兴趣的a blog post on S3/S4 dispatch performance