在R中对向量进行引用子分配

18

我可以以某种方式通过引用对原子向量进行子赋值吗?当然,不需要将其包装在一个1列数据表中以使用:=

library(data.table)
N <- 5e7
x <- sample(letters, N, TRUE)
X <- data.table(x = x)
upd_i <- sample(N, 1L, FALSE)
system.time(x[upd_i] <- NA_character_)
#    user  system elapsed 
#    0.11    0.06    0.17 
system.time(X[upd_i, x := NA_character_])
#    user  system elapsed 
#    0.00    0.00    0.03 
如果R6可以帮助解决这个问题,我很乐意采用R6的解决方案,因为它已经是我的依赖之一。我已经检查过,在R6对象内部使用"<-"仍然会进行复制,请参考gist

有趣。从来没听说过 R6。看起来很令人兴奋。 - David Arenburg
1
@DavidArenburg R6 在 R 语言中是我认为最好的参考类和面向对象编程工具。绝对值得学习,而且易于学习。 - jangorecki
我猜你可以用Rcpp通过引用进行修改。谷歌搜索结果为:https://dev59.com/H2gu5IYBdhLWcg3we2-t,那里链接的Ari的回答有一个简单的函数,它的性能比其他函数都要好。 - Frank
2个回答

9
在最近的R版本(3.1-3.1.2+左右),向量赋值不会复制。然而,通过运行OP代码将看不到这一点,原因是因为您正在重用x并将其分配给其他对象时,R未被通知此时x已被复制,并且必须假设它不会被复制(在上面的特定情况下,我认为在data.table :: data.table中更改它并通知R已经进行了复制会很好,但这是一个单独的问题 - data.frame 遇到相同的问题),因此在第一次使用时会复制x。如果您稍微更改命令的顺序,您将看不到任何区别:
N <- 5e7
x <- sample(letters, N, TRUE)
upd_i <- sample(N, 1L, FALSE)
# no copy here:
system.time(x[upd_i] <- NA_character_)
#   user  system elapsed 
#      0       0       0 
X <- data.table(x = x)
system.time(X[upd_i, x := NA_character_])
#   user  system elapsed 
#      0       0       0 

# but now R will copy:
system.time(x[upd_i] <- NA_character_)
#   user  system elapsed 
#   0.28    0.08    0.36 

(旧答案,仅供参考)

实际上,你可以使用data.table:=操作符直接在原地修改向量(我认为你需要R版本3.1+以避免在list中进行复制):

modify.vector = function (v, idx, value) setDT(list(v))[idx, V1 := value]

v = 1:5
address(v)
#[1] "000000002CC7AC48"

modify.vector(v, 4, 10)
v
#[1]  1  2  3 10  5

address(v)
#[1] "000000002CC7AC48"

完全同意这个解决方案,我进行了基准测试,速度超快。@eddi 我认为这是一个很好的PR功能。 - jangorecki
嗯,我不确定为什么这个有效...也就是说,如果你执行res <- setDT(list(v))[4, V1 := 10],你会得到一个data.table而不是向量。我曾经问过一个类似的问题,并被R开发人员告知这不应该起作用。 - David Arenburg
@DavidArenburg 它能够工作的原因是listsetDT都没有复制底层数据。它们确实创建了新对象,但这些对象只是包装了数据,所以res是一个新对象,但其中包含原始向量。 - eddi
2
@jangorecki 嗯,虽然这个答案可能只适用于R 3.1+,但实际上它已经过时了,因为我非常确定现在对于常规向量赋值不会再产生额外的副本,即 v[4] = 20 不会产生任何不必要的副本。 - eddi

5

如@Frank所建议的,可以使用Rcpp来实现这一功能。以下是一个版本,包含受Rcpp的dispatch.h启发的宏,可处理所有原子向量类型:

mod_vector.cpp

#include <Rcpp.h>
using namespace Rcpp;

template <int RTYPE>
Vector<RTYPE> mod_vector_impl(Vector<RTYPE> x, IntegerVector i, Vector<RTYPE> value) {
  if (i.size() != value.size()) {
    stop("i and value must have same length.");
  }
  for (int a = 0; a < i.size(); a++) {
    x[i[a] - 1] = value[a];
  }
  return x;
}

#define __MV_HANDLE_CASE__(__RTYPE__) case __RTYPE__ : return mod_vector_impl(Vector<__RTYPE__>(x), i, Vector<__RTYPE__>(value));

// [[Rcpp::export]]
SEXP mod_vector(SEXP x, IntegerVector i, SEXP value) {
  switch(TYPEOF(x)) {
    __MV_HANDLE_CASE__(INTSXP)
    __MV_HANDLE_CASE__(REALSXP)
    __MV_HANDLE_CASE__(RAWSXP)
    __MV_HANDLE_CASE__(LGLSXP)
    __MV_HANDLE_CASE__(CPLXSXP)
    __MV_HANDLE_CASE__(STRSXP)
    __MV_HANDLE_CASE__(VECSXP)
    __MV_HANDLE_CASE__(EXPRSXP)
  }
  stop("Not supported.");
  return x;
}

例子:

x <- 1:20
address(x)
#[1] "0x564e7e8"
mod_vector(x, 4:5, 12:13)
# [1]  1  2  3 12 13  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
address(x)
#[1] "0x564e7e8"

与基础和data.table方法的比较。可以看出,它要快得多:
x <- 1:2e7
microbenchmark::microbenchmark(mod_vector(x, 4:5, 12:13), x[4:5] <- 12:13, modify.vector(x, 4:5, 12:13))
#Unit: microseconds
#                         expr     min       lq        mean    median         uq
#    mod_vector(x, 4:5, 12:13)   5.967   7.3480    15.05259     9.718    21.0135
#              x[4:5] <- 12:13   2.953   5.3610 45722.61334 48122.996 52623.1505
# modify.vector(x, 4:5, 12:13) 954.577 988.7785  1177.17925  1021.380  1361.1210
#        max neval
#     58.463   100
# 126978.146   100
#   1559.985   100

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