在 Rcpp 函数中替换 Rcpp::List 的元素是内存安全的吗?

Is replacing element of Rcpp::List inside an Rcpp function memory-safe?

我需要覆盖作为参数传递给 Rcpp 函数的 Rcpp::List 对象的元素。我担心的是内存安全。是不是通过重新分配列表的非空元素,我有效地重新连接了一个指向原始内容的指针,但从未释放存储原始内容的内存?如果是,如何解决?

我知道我可以轻松修改作为 Rcpp::List 元素的 Rcpp 对象(例如 Rcpp::NumericVector),因为 Rcpp::NumericVector 进行浅拷贝。然而,这不满足我的要求,即用其他东西完全替换元素。

下面,我包含了一个 C++ 代码片段,它显示了我所指的场景。

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
void replaceListElement(List l)
{
    std::vector<int> v;
    v.push_back(4);
    v.push_back(5);
    v.push_back(6);
    l["a"] = v;
}

/*** R
l <- list()
l$a <- c(1,2,3)
replaceListElement(l)
print(l)
*/

在 RStudio 中通过 Rcpp 获取时,print(l) 命令输出以下内容

$a
[1] 4 5 6

这是期望的结果,所以我的问题仅与内存安全有关。

A Rcpp::List 是一个 Vector<VECSXP>,即指向其他向量的指针向量。如果你为这个列表中的某个元素分配一个新的向量,你实际上只是在改变一个指针而没有释放指针用来指向的内存。然而,R 仍然知道这个内存并通过它的垃圾收集器释放它。我们可以通过一个简单的实验看到这一点,在这个实验中我使用你的 C++ 代码,对 R 代码稍作改动:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
void replaceListElement(List l)
{
  std::vector<int> v;
  v.push_back(4);
  v.push_back(5);
  v.push_back(6);
  l["a"] = v;
}

/*** R
l <- list()
l$a <- runif(1e7)
replaceListElement(l)
print(l)
gc() # optional
*/

这里使用了更大的向量,使效果更加突出。如果我现在使用 R -d valgrind -e 'Rcpp::sourceCpp("<filename>")' 我会通过 gc() 调用

得到以下结果
==13827==
==13827== HEAP SUMMARY:
==13827==     in use at exit: 48,125,775 bytes in 9,425 blocks
==13827==   total heap usage: 34,139 allocs, 24,714 frees, 173,261,724 bytes allocated
==13827==
==13827== LEAK SUMMARY:
==13827==    definitely lost: 0 bytes in 0 blocks
==13827==    indirectly lost: 0 bytes in 0 blocks
==13827==      possibly lost: 0 bytes in 0 blocks
==13827==    still reachable: 48,125,775 bytes in 9,425 blocks
==13827==                       of which reachable via heuristic:
==13827==                         newarray           : 4,264 bytes in 1 blocks
==13827==         suppressed: 0 bytes in 0 blocks
==13827== Rerun with --leak-check=full to see details of leaked memory
==13827==
==13827== For counts of detected and suppressed errors, rerun with: -v
==13827== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

并且没有 gc() 调用:

==13761==
==13761== HEAP SUMMARY:
==13761==     in use at exit: 132,713,314 bytes in 10,009 blocks
==13761==   total heap usage: 34,086 allocs, 24,077 frees, 173,212,886 bytes allocated
==13761==
==13761== LEAK SUMMARY:
==13761==    definitely lost: 0 bytes in 0 blocks
==13761==    indirectly lost: 0 bytes in 0 blocks
==13761==      possibly lost: 0 bytes in 0 blocks
==13761==    still reachable: 132,713,314 bytes in 10,009 blocks
==13761==                       of which reachable via heuristic:
==13761==                         newarray           : 4,264 bytes in 1 blocks
==13761==         suppressed: 0 bytes in 0 blocks
==13761== Rerun with --leak-check=full to see details of leaked memory
==13761==
==13761== For counts of detected and suppressed errors, rerun with: -v
==13761== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

所以在这两种情况下 valgrind 都没有检测到任何内存泄漏。仍可访问的内存量相差大约 8x10^7 字节,即 l$a 中原始向量的大小。这表明 R 确实知道原始向量并在被告知时释放它,但是当 R 自己决定 运行 垃圾收集器时也会发生这种情况。