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

时间:2013-12-03 10:54:37

标签: 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()

产生以下输出:

> 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

循环后添加了大约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

从赋值中删除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

总结一下,有两个问题:

  1. 我错过了什么或是否有内存泄漏?
  2. 如果确实存在内存泄漏,是否有人可以建议一种解决方法,让我按组引用使用赋值而不会泄漏内存?
  3. 供参考,这是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
    

1 个答案:

答案 0 :(得分:6)

来自Matt的更新 - 现已修复于v1.8.11。来自NEWS

  

分组固定时长时间(通常很小)内存泄漏。什么时候   最后一组小于最大组,差异在于   那些尺寸没有被释放。大多数用户运行分组查询   曾经且永远不会注意到,但是有人将呼叫循环到分组   (例如当并行运行或基准测试时)可能遭受了损失,    #2648。测试补充。

     

非常感谢vc273,Y T和其他人。



来自阿伦...

为什么会这样?

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


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

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

简短的解释是,这似乎是在data.table的SETLENGTH中使用dogroups.c函数(来自R的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]

对应id=1,必须选择“x”(=c(1,2,1,1,2,3))的值。这意味着,必须为每个.SD值的byby}以外的所有列)分配内存。

为了克服by中每个群组的这种分配,data.table通过首先在.SD中分配by最大群组的长度来巧妙地实现这一点(这里是对应id=1,长度6)。然后,我们可以为id的每个值,重新使用(过度)分配的data.table并使用函数SETLENGTH我们可以将长度调整为当前组的长度。请注意,通过这样做,这里没有实际分配内存,除了为最大组分配的内存。

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

为了说明这一点,我在`data.table中编写了一个模仿<{em} SETLENGTH dogroups.c函数用法的C代码。

// 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);
}

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

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

R CMD SHLIB -o test.so test.c

现在,打开一个新的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())
}

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

通过运行此代码,您会发现1)返回的两个值对于iaddress(SD)的每个Vcells used Mb都是相同的,并且{2} 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 ,您将发现并非所有内存都被还原/释放。

初​​始:

          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

100次运行后:

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 之后:

SETLENGTH(SD, ...)

如果从C代码中删除行{{1}}并再次运行,您会发现Vcells没有变化。

现在为什么 SETLENGTH对具有不同组长度的分组有这种影响,我仍然试图理解 - 检查编辑以上。