警告:在向函数返回的data.table中添加列时,“检测到无效的.internal.selfref”

时间:2013-12-19 16:48:21

标签: r data.table

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

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

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
}

2 个答案:

答案 0 :(得分:24)

这与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版本&gt; = 3.1.0(现在在开发中),将命名对象传递给list()不再创建副本,而是引用计数(指向此值的对象数)刚刚被碰撞。因此,问题随着下一版本的R而消失。

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

首先,一些历史:

您应该知道data.table在创建时过度分配列指针槽(truelength设置为默认值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.table&#39; s copy())的问题是R内部将truelength参数设置为0甚至虽然truelength(.)函数仍将返回正确的结果。这通过:=引用更新时无意中导致段错误,因为超额分配不再存在(或者至少不存在)已经认出了)。这发生在版本&lt; 1.7.8。为了克服这个问题,引入了一个名为.internal.selfref的属性。您可以通过执行attributes(DT)来检查此属性。

来自NEWS(第1.7.8节):

  

o克里斯崩溃&#39;是固定的。根本原因是key<-总是复制整个表。 该副本的问题(除了速度较慢)是R不维护过度分配的truelength,但它看起来好像有。内部使用了key<-,特别是merge()。因此,在:=之后使用merge()添加列是内存覆盖,因为在key<-复制后,过度分配的内存确实不存在。

     

data.tables现在有了一个新属性.internal.selfref,可以在将来捕获并警告此类副本。 key<-的所有内部使用都已替换为setkey()或接受向量的新函数setkeyv(),并且不会复制。

.internal.selfref做了什么?

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

.internal.selfref如何实施?

为了理解这个属性.internal.selfref,我们必须了解外部指针EXTPTRSXP)是什么。 This page很好地解释了。复制/粘贴基本行:

  

外部指针SEXP旨在处理对诸如 handle 之类的C结构的引用,并且例如在包RODBC中用于此目的。它们的复制语义不同寻常,因为复制R对象时,外部指针对象不会重复。

它们被创建为:

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

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

在我们的例子中,我们为DT创建/的属性.internal.selfref,其值是一个指向NULL的外部指针(你在属性值中看到的地址)和这个外部指针&#39; s { {1}}字段是另一个返回prot的外部指针(因此称为 selfref ),此次DT设置为NULL。

注意:我们已将此extptr用于NULL,其中&#39; prot&#39;是一个extptr策略,以便prot是两个不同的副本,但具有相同的内容返回TRUE。 (如果你不明白这意味着什么,你可以跳到下一部分。它与理解这个问题的答案无关。)

好的,这一切如何运作呢?

我们知道在复制过程中外部指针不会重复。基本上,当我们创建data.table时,属性.internal.selfref会创建一个指向NULL的外部指针,并使用identical(DT1, DT2)字段创建一个返回prot的外部指针。现在,当一个无意的&#34;副本&#34;正在制作,对象的地址被修改但不受属性保护的地址。它仍然指向DT是否存在......因为它不会被修改。因此,通过检查当前对象的地址和外部指针保护的地址,可以在内部检测到这一点。如果他们不匹配,那么&#34;复制&#34; R已经完成(可能会丢失过度分配的data.table精心创建)。那就是:

DT

要接受很多东西。我想我已经设法尽可能清楚地完成它。如果出现任何错误(我需要一段时间将其包裹在我的脑海中)或进一步明确的可能性,请随时编辑或评论您的建议。

希望这可以解决问题。

答案 1 :(得分:9)

阿伦的答案是一个很好的解释。 R&lt; = 3.0.2中list()的具体特征是它复制命名输入(在调用list()之前已经命名的事物)。现在在r-devel(R的下一个版本)中,list()的副本不再发生,一切都会很好。这是R中非常受欢迎的变化。

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

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

首先演示list()复制:

> 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()的副本。