data.table中的内存泄漏按引用分组分配

yts*_*aig 7 r data.table

我在组中使用按组引用分配时看到奇数内存使用情况data.table.这是一个简单的示例(请原谅示例的无关紧要):

N <- 1e6
dt <- data.table(id=round(rnorm(N)), value=rnorm(N))

gc()
for (i in seq(100)) {
  dt[, value := value+1, by="id"]
}
gc()
tables()
Run Code Online (Sandbox Code Playgroud)

产生以下输出:

> gc()
used (Mb) gc trigger (Mb) max used (Mb)
Ncells  303909 16.3     597831 32.0   407500 21.8
Vcells 2442853 18.7    3260814 24.9  2689450 20.6
> for (i in seq(100)) {
  +   dt[, value := value+1, by="id"]
  + }
> gc()
used  (Mb) gc trigger  (Mb) max used  (Mb)
Ncells   315907  16.9     597831  32.0   407500  21.8
Vcells 59966825 457.6   73320781 559.4 69633650 531.3
> tables()
NAME      NROW MB COLS     KEY
[1,] dt   1,000,000 16 id,value    
Total: 16MB
Run Code Online (Sandbox Code Playgroud)

因此在循环之后添加了大约440MB的使用过的Vcells内存.从内存中删除data.table后,不考虑此内存:

> rm(dt)
> gc()
used  (Mb) gc trigger (Mb) max used  (Mb)
Ncells   320888  17.2     597831   32   407500  21.8
Vcells 57977069 442.4   77066820  588 69633650 531.3
> tables()
No objects of class data.table exist in .GlobalEnv
Run Code Online (Sandbox Code Playgroud)

从赋值中删除by = ...时,内存泄漏似乎消失了:

>     gc()
used (Mb) gc trigger (Mb) max used (Mb)
Ncells  312955 16.8     597831 32.0   467875 25.0
Vcells 2458890 18.8    3279586 25.1  2704448 20.7
>     for (i in seq(100)) {
  +       dt[, value := value+1]
  +     }
>     gc()
used (Mb) gc trigger (Mb) max used (Mb)
Ncells  322698 17.3     597831 32.0   467875 25.0
Vcells 2478772 19.0    5826337 44.5  5139567 39.3
>     tables()
NAME      NROW MB COLS     KEY
[1,] dt   1,000,000 16 id,value    
Total: 16MB
Run Code Online (Sandbox Code Playgroud)

总结一下,有两个问题:

  1. 我错过了什么或是否有内存泄漏?
  2. 如果确实存在内存泄漏,是否有人可以建议一种解决方法,让我按组引用使用赋值而不会出现内存泄漏?

供参考,这是输出sessionInfo():

R version 3.0.2 (2013-09-25)
Platform: x86_64-pc-linux-gnu (64-bit)

locale:
  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8     LC_MONETARY=en_US.UTF-8   
[6] LC_MESSAGES=en_US.UTF-8    LC_PAPER=en_US.UTF-8       LC_NAME=C                  LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
  [1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
  [1] data.table_1.8.10

loaded via a namespace (and not attached):
  [1] tools_3.0.2
Run Code Online (Sandbox Code Playgroud)

Aru*_*run 6

来自马特的更新 - 现在修复于v1.8.11.来自新闻:

分组固定时长时间(通常很小)内存泄漏.当最后一组小于最大组时,这些大小的差异未被释放.大多数用户运行一次分组查询并且永远不会注意到,但是任何循环调用分组的人(例如并行运行或基准测试时)都可能遭受损失,#2648.测试补充.

非常感谢vc273,YT和其他人.



从阿伦...

为什么会这样?

我希望在谈到这个问题之前我曾经遇到过这个帖子.然而,一个很好的学习经历.Simon Urbanek非常简洁地总结了这个问题,它不是内存泄漏,而是使用/释放内存的错误报告.我感觉这就是发生的事情.


发生这种情况的原因是什么data.table?这部分是dogroups.c为了确定代码的一部分,负责显着的内存增加.

好吧,经过一些繁琐的测试后,我想我至少找到了导致这种情况发生的原因.希望有人可以帮助我从这篇文章到达那里.我的结论是,这是不是一个内存泄漏.

简短的解释是,这似乎是SETLENGTHdata.table 中函数(来自R的C接口)的使用效果dogroups.c.

data.table,当您使用时by=...,例如,

set.seed(45)
DT <- data.table(x=sample(3, 12, TRUE), id=rep(3:1, c(2,4,6)))
DT[, list(y=mean(x)), by=id]
Run Code Online (Sandbox Code Playgroud)

对应于id=1,=c(1,2,1,1,2,3)必须选择"x"()的值.这意味着,必须为每个值分配.SD(所有列不在by)的内存by.

在克服这种分配对各组by,data.table由第一分配巧妙地实现此目的.SD具有最大组中的长度by(其在这里被对应于id=1,长度6).然后,我们可以是,的每个值id,再利用的(过度)分配data.table并通过使用函数SETLENGTH,我们可以只调整长度以当前组的长度.请注意,通过执行此操作,此处不会实际分配任何内存,除了为最大组分配的内存.

但看起来很奇怪的是,当每个组中的元素数量by都具有相同数量的项目时,在gc()输出方面似乎没有什么特别的事情发生.但是,当它们不相同时,gc()似乎报告了Vcells的使用量增加.尽管在这两种情况下都没有分配额外的内存.

为了说明这一点,我写了一个C-代码,模仿SETLENGTH功能使用中dogroups.c的`data.table.

// test.c
#include <R.h>
#define USE_RINTERNALS
#include <Rinternals.h>
#include <Rdefines.h>

int sizes[100];
#define SIZEOF(x) sizes[TYPEOF(x)]

// test function - no checks!
SEXP test(SEXP vec, SEXP SD, SEXP lengths)
{
    R_len_t i, j;
    char before_address[32], after_address[32];
    SEXP tmp, ans;
    PROTECT(tmp = allocVector(INTSXP, 1));
    PROTECT(ans = allocVector(STRSXP, 2));
    snprintf(before_address, 32, "%p", (void *)SD);
    for (i=0; i<LENGTH(lengths); i++) {
        memcpy((char *)DATAPTR(SD), (char *)DATAPTR(vec), INTEGER(lengths)[i] * SIZEOF(tmp));
        SETLENGTH(SD, INTEGER(lengths)[i]);
        // do some computation here.. ex: mean(SD)
    }
    snprintf(after_address, 32, "%p", (void *)SD);
    SET_STRING_ELT(ans, 0, mkChar(before_address));
    SET_STRING_ELT(ans, 1, mkChar(after_address));
    UNPROTECT(2);
    return(ans);
}
Run Code Online (Sandbox Code Playgroud)

这里vec是等同于任何data.table dtSD等效于.SDlengths位于每个组的长度.这只是一个虚拟程序.基本上对于每个值lengths,比如说n,第一个n元素都是从vec上复制到的SD.然后可以计算出这个SD上想要的任何东西(这里没有做到).出于我们的目的,返回使用SETLENGTH操作之前和之后的SD地址,以说明SETLENGTH没有复制.

将此文件另存为test.c,然后从终端编译如下:

R CMD SHLIB -o test.so test.c
Run Code Online (Sandbox Code Playgroud)

现在,打开一个新的R-session,转到test.so存在的路径,然后键入:

dyn.load("test.so")
require(data.table)
set.seed(45)
max_len <- as.integer(1e6)
lengths <- as.integer(sample(4:(max_len)/10, max_len/10))
gc()
vec <- 1:max_len
for (i in 1:100) {
    SD <- vec[1:max(lengths)]
    bla <- .Call("test", vec, SD, lengths)
    print(gc())
}
Run Code Online (Sandbox Code Playgroud)

请注意,对于每个i这里,.SD将分配一个不同的内存位置,并通过SD为每个位置分配来复制i.

通过运行此代码,您将发现1)返回的两个值对于每个值都是相同的i,address(SD)并且2)Vcells used Mb保持增加.现在,从工作区中删除所有变量rm(list=ls()),然后执行gc(),您会发现并非所有内存都被恢复/释放.

初始:

          used (Mb) gc trigger (Mb) max used (Mb)
Ncells  332708 17.8     597831 32.0   467875 25.0
Vcells 1033531  7.9    2327578 17.8  2313676 17.7
Run Code Online (Sandbox Code Playgroud)

100次运行后:

          used (Mb) gc trigger (Mb) max used (Mb)
Ncells  332912 17.8     597831 32.0   467875 25.0
Vcells 2631370 20.1    4202816 32.1  2765872 21.2
Run Code Online (Sandbox Code Playgroud)

之后rm(list=ls())gc():

          used (Mb) gc trigger (Mb) max used (Mb)
Ncells  341275 18.3     597831 32.0   467875 25.0
Vcells 2061531 15.8    4202816 32.1  3121469 23.9
Run Code Online (Sandbox Code Playgroud)

如果SETLENGTH(SD, ...)从C代码中删除该行并再次运行,您会发现Vcells没有变化.

现在,为什么 SETLENGTH对具有不同组长度的分组具有这种效果,我仍然试图理解 - 检查上面编辑中的链接.

  • 实际上它可能是一个缓慢的实际泄漏.类似的泄漏由assign.c顶部的终结器修复,但是之后的一些报告表明问题仍然存在,但我从来没有到底(例如[这里](http://stackoverflow.com/questions/15651515 /慢内存泄漏,在数据表 - 当 - 返回 - 命名列表式-J-试图对reshap)).现在再次用新鲜的眼睛看,dogroups.c似乎在返回之前在`.SD`和`I`的末尾错过了一个`SETLENGTH`回到它们的原始长度(最大组的大小).如果就是这样的话,我会因为之前没有发现它而踢我自己! (2认同)