r语言 - 警告:向从函数返回的 data.table 添加列时'Invalid .internal.selfref detected'



这似乎是fread错误,但我不确定。

这个例子再现了我的问题。我有一个函数,读取data.table并将其返回到列表中。我使用列表将其他结果分组到相同的结构中。这是我的代码:

ff.fread <- function(){
dt = fread("x
1
2
")
list(dt=dt)   
}
DT.f <- ff.fread()$dt

现在,当我尝试向DT.f添加一个新列时,它可以工作,但我收到了一条警告消息:

DT.f[,y:=1:2]
Warning message:
In `[.data.table`(DT.f, , `:=`(y, 1:2)) :
Invalid .internal.selfref detected and fixed by taking a copy of the whole
table so that := can add this new column by reference. At an earlier point,
this data.table has been copied by R (or been created manually using
structure() or similar). Avoid key<-, names<- and attr<- which in R currently
(and oddly) may copy the whole data.table. Use set* syntax instead to avoid
copying: ?set, ?setnames and ?setattr. Also, in R<v3.1.0, list(DT1,DT2) copied
the entire DT1 and DT2 (R's list() used to copy named objects); please upgrade
to R>=v3.1.0 if that is biting. If this message doesn't help, please report to
datatable-help so the root cause can be fixed.

请注意,如果我手动创建data.table,则不会出现此警告。这很好,例如:

ff <- function(){
list(dt=data.table(x=1:2))
}
DT <- ff()$dt
DT[,y:=1:2]

或者,如果我不在列表中返回fread的结果,它也可以很好地

ff.fread <- function(){
dt = fread("x
1
2
")
dt
}

这与fread本身无关,而是调用list()并向其传递一个命名对象。我们可以通过以下操作重新创建:

require(data.table)
DT <- data.table(x=1:2)       # name the object 'DT'
DT.l <- list(DT=DT)           # create a list containing one data.table
y <- DT.l$DT                  # get back the data.table
y[, bla := 1L]                # now add by reference
# works fine but warning message will occur
DT.l = list(DT=data.table(x=1:2))   # DT = a call, not a named object
y = DT.l$DT
y[, bla:=1L]
# works fine and no warning message

好消息:

好消息是,从R版本>=3.1.0(现在在devel中),将命名对象传递给list()将不再创建副本,相反,它的引用计数(指向该值的对象数)会被打乱。因此,问题随着R.的下一个版本而消失

为了了解data.table如何使用.internal.selfref检测副本,我们将深入了解data.table的一些历史。

首先,一些历史:

您应该知道,data.table在创建时会过度分配列指针槽(truellength设置为默认值100),因此:=可以用于稍后通过引用添加列。这其中有一个问题-处理副本。例如,当我们调用list()并将一个命名对象传递给它时,就会生成一个副本,如下所示。

tracemem(DT)
# [1] "<0x7fe23ac3e6d0>"
DT.list <- list(DT=DT)    # `DT` is the named object on the RHS of = here
# tracemem[0x7fe23ac3e6d0 -> 0x7fe23cd72f48]: 

R制作的data.table的任何副本(而不是data.tablecopy())的问题是,即使truelength(.)函数仍然会返回正确的结果,R也会在内部将truelength参数设置为0。当通过引用:=进行更新时,这无意中导致了segfault,因为过度分配不再存在(或者至少不再被识别)。这发生在<1.7.8.为了克服这个问题,引入了一个名为.internal.selfref的属性。您可以通过执行attributes(DT)来检查此属性。

来自新闻(v1.7.8):

o"Chris崩溃"已修复。根本原因是key<-总是复制整个表该副本的问题(除了速度较慢之外)是R没有维护过度分配的truelength,但看起来它有key<-在内部使用,特别是在merge()中。因此,在merge()之后添加一个使用:=的列是一种内存覆盖,因为在key<-的复制之后,过度分配的内存实际上并不存在。

CCD_ 27现在有一个新的属性CCD_。key<-的所有内部使用都已被setkey()或新函数setkeyv()取代,后者接受向量,并且不复制。

这个.internal.selfref做什么

基本上,它只是指向自己。它只是附加到DT的一个属性,包含DT的RAM中的地址。如果R无意中复制了DTDT的地址将在RAM中移动,但附加的属性仍将包含旧的内存地址,它们将不再匹配。data.table检查它们是否匹配(即有效),然后通过引用将新列添加到备用列指针槽中。

.internal.selfref是如何实现的

为了理解这个属性.internal.selfref,我们必须理解外部指针(EXTPTRSXP)是什么。这个页面很好地解释了这一点。复制/粘贴基本线条:

外部指针SEXP用于处理对C结构的引用,例如句柄,并在包ROBC中用于此目的。它们的复制语义不同寻常,因为当复制R对象时,外部指针对象不会被复制。

它们被创建为:

SEXP R_MakeExternalPtr(void *p, SEXP tag, SEXP prot);

其中p是指针(因此它不能是函数指针),tag和prot是对普通R对象的引用,这些对象将在外部指针对象的生存期内保持存在(受到保护,不会被垃圾收集)。一个有用的约定是,如果外部指针所代表的内存是从R堆中分配的,则使用标记字段来进行某种形式的类型标识,使用prot字段来保护该内存。

在我们的例子中,我们为DT创建了/的属性.internal.selfref,其值是指向NULL的外部指针(您可以在属性值中看到其地址),该外部指针的prot字段是指向DT的另一个外部指针(因此称为selfref),此时其prot设置为NULL。

注意:我们必须使用这个extptr到NULL,它的"prot"是一个extptr策略,这样identical(DT1, DT2)(两个不同的副本,但内容相同)就会返回TRUE。(如果你不明白这意味着什么,你可以跳到下一部分。这与理解这个问题的答案无关)。

好吧,那这一切是怎么回事

我们知道外部指针在复制过程中不会被复制。基本上,当我们创建data.table时,属性.internal.selfref创建了一个指向NULL的外部指针,它的prot字段创建了返回DT的外部指针。现在,当一个无意的";复制";在生成时,对象的地址会被修改,但不会修改受属性保护的地址。它仍然指向DT,不管它是否存在。。因为它不会/不能被修改。因此,这是通过检查当前对象的地址和由外部指针保护的地址在内部检测到的。如果它们不匹配;复制";已经由R创建(这将丢失data.table精心创建的过度分配)。即:

DT <- data.table(x=1:2) # internal selfref set
DT.list <- list(DT=DT)  # copy made, address(DT.list$DT) != address(DT)
# and truelength would be affected.
DT.new <- DT.list$DT    # address of DT.new != address of DT
# and it's not equal to the address pointed to by
# the attribute's 'prot' external pointer
# so a re-over-allocation has to be made by data.table at the next update by
# reference, and it warns so you can fix the root cause by not using list(),
# key<-, names<- etc.

这有很多需要接受的。我想我已经尽可能清楚地度过了难关。如果有任何错误(我花了一段时间才把它记在脑子里)或进一步澄清的可能性,请随时编辑或评论您的建议。

希望这能澄清问题。

Arun的回答是一个很好的解释。CCD_ 49在R&lt3.0.2是复制命名的输入(在调用list()之前已经命名的东西)。现在在r-devel(r的下一个版本)中,list()的这种复制不再发生,一切都会好起来。这是R.非常受欢迎的变化

同时,您可以通过以不同的方式创建输出列表来解决此问题。

> R.version.string
[1] "R version 3.0.2 (2013-09-25)"

首先演示列表()复制:

> DT = data.table(a=1:3)
> address(DT)
[1] "0x1d70010"
> address(list(DT)[[1]])
[1] "0x21bc178"    # different address => list() copied the data.table named DT
> data.table:::selfrefok(DT)
[1] 1
> data.table:::selfrefok(list(DT)[[1]])
[1] 0              # i.e. this copied DT is not over-allocated

现在有一种不同的方式来创建相同的列表:

> ans = list()
> ans$DT = DT    # use $<- instead
> address(DT)
[1] "0x1d70010"
> address(ans$DT)
[1] "0x1d70010"    # good, no copy
> identical(ans, list(DT=DT))
[1] TRUE
> data.table:::selfrefok(ans$DT)
[1] 1              # good, the list()-ed DT is still over-allocated ok

我知道,我很困惑。使用$<-创建输出列表,或者甚至只是将对fread的调用放在对list()的调用中,即list(DT=fread(...))应该避免list()的复制。

最新更新