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

Posted

技术标签:

【中文标题】data.table 中的内存泄漏按引用分组分配【英文标题】:Memory leak in data.table grouped assignment by reference 【发布时间】:2013-12-03 10:54:37 【问题描述】:

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

总结一下,两个问题:

    是我遗漏了什么还是存在内存泄漏? 如果确实存在内存泄漏,任何人都可以提出一种解决方法,让我使用按组引用分配而不会发生内存泄漏?

作为参考,这里是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

【问题讨论】:

如果可能存在错误,请说明您使用的 R 和 data.table 的版本。 似乎仅在元素/组的数量不均匀时才会发生。如果你这样做:dt &lt;- data.table(id=1:N, value=rnorm(N)) 或任何其他方式,以便每个 id 值的元素数是恒定的,那么这不会发生。 @flodel - 谢谢,我将编辑帖子以包含这些详细信息。 【参考方案1】:

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

修复了分组中长期未完成(通常很小)的内存泄漏。什么时候 最后一组小于最大组,差 这些尺寸没有发布。大多数用户运行分组查询 一次并且永远不会注意到,但是任何人循环调用分组 (例如在并行运行或基准测试时)可能会受到影响, #2648.已添加测试。

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



来自阿伦...

为什么会这样?

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


data.table 发生这种情况的原因是什么?这部分是关于从dogroups.c 中识别导致内存明显增加的代码部分。

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

简短的解释是,这似乎是在 data.table 的 dogroups.c 中使用 SETLENGTH 函数(来自 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))的值。这意味着,必须根据by 值为.SD(不在by 中的所有列)分配内存。

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

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

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

// 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 将被分配一个不同的内存位置,并且通过为每个 i 分配 SD 在此处复制。

通过运行此代码,您会发现 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

运行 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

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

如果您从 C 代码中删除 SETLENGTH(SD, ...) 行,然后再次运行,您会发现 Vcell 没有任何变化。

现在为什么SETLENGTH 对具有不同组长度的分组会产生这种影响,我仍在努力理解 - 查看编辑以上。

【讨论】:

感谢您深入了解并找出原因。除了您上面提出的测试之外,我还通过将迭代次数提高到 1000 次并使用操作系统工具而不是 gc() 监控内存使用情况,进行了更强力的测试。事实上,最后 gc 报告了超过 5GB 的已用内存,而操作系统报告 R 从未使用超过几百 MB。还要感谢指向 R 开发帖子的有用指针,尽管得知我们不能相信来自 gc() 的数字有点令人沮丧,尤其是考虑到 R 的内存需求如此之大。 实际上可能是一个缓慢的真正泄漏。 assign.c 顶部的终结器修复了类似的泄漏,但此后的一些报告表明问题仍然存在,但我从未深入了解它(例如here)。现在用新的眼光再看一遍,dogroups.c 似乎在.SDI 的末尾缺少了一个SETLENGTH,在返回之前又回到了原来的长度(最大组的大小)。如果仅此而已,我会因为之前没有发现这一点而自责! @MattDowle,嗯,我的 mac 上的 r-session 进程使用的内存(或任何空闲/活动/非活动内存)似乎绝对没有增加。这使我认为这实际上不是泄漏。话虽如此,我没有意识到简单地设置长度可以确保内存的“正确”报告。太精彩了!我会检查一下。 如果您在可能 30 分钟后对较大的数据集(并且最大的组需要更大)重复执行 DT[,j,by],您应该会看到 R 的 RAM 使用量增加,即泄漏。最好每次都有一个新的 R 会话来查看它。如果这个理论是正确的,那么一个包含两组的大表,其中一组只有 1 行,其他 n-1 行,并且 1 行组由 dogroups 第二个计算 - 这应该泄漏最严重。如果正确,最后一组相对于最大组的大小应该会有所不同。如果是这样,将SETLENGTH 添加到 dogroup 的末尾应该可以修复泄漏。 是否可以保证当您减少长度时,其余数据不会被覆盖?

以上是关于data.table 中的内存泄漏按引用分组分配的主要内容,如果未能解决你的问题,请参考以下文章

如果名称按组的顺序不同,R data.table 分组操作返回错误值?

data.table 包中的 := (按引用传递)运算符同时修改另一个数据表对象

如何根据每周日期创建移动平均线,按data.table中的多列分组?

前端如何处理内存泄漏

它是C中的内存泄漏吗?

Java内存泄漏解析!