RcppArmadillo:内存使用问题

5

我开始使用Rcpp, 我非常喜欢它。但是我对编程还比较陌生,我有一个关于内存使用的问题。以下是一个可重现的问题:

library(RcppArmadillo)
library(inline)
code <- "
  Rcpp::NumericVector input_(input);
  arma::cube disturb(input_.begin(), 2, 2, 50000000, false);
  return wrap(2);
"
Test <- cxxfunction(signature(input = "numeric"), plugin = "RcppArmadillo", body = code)
input <- array(rnorm(2 * 2 * 50000000), dim = c(2, 2, 50000000))
Test(input)

我的理解是,在上述问题中,唯一使用内存的情况是当我在R中将数组分配给变量input时。因此,我应该只使用约1.6 GB(2 * 2 * 50 * 8 = 1600)。当我转到Rcpp时,我使用SEXP对象初始化变量input_,它是一个指针。因此,这不应该使用任何额外的内存。然后,当我初始化disturb变量时,我还使用一个指针,并设置copy_aux = FALSE。因此,我不应该使用任何内存。因此,如果我的理解正确,当我运行代码时,我应该只使用1.6 GB。这个理解正确吗?
然而,当我运行代码时,内存使用量(通过查看Ubuntu中的系统监视器判断)会跳到超过10 GB(从约1 GB开始),然后再回落至大约4 GB。我不明白发生了什么。我是否错误地使用了Rcpp?
感谢您的帮助。非常感谢。

我使用 sourceCpp() 和 R 包测试了同样的问题。当我将大型对象传递给 C++ 时,我遇到了相同的意外大内存使用问题。 - master_goon
2
这似乎不是arma :: mat的问题,但我使用arma :: cube时遇到了完全相同的问题。这可能与Rcpp中缺少适当的3D数组类有关吗? - Nick Kennedy
是的。它适用于arma::mat。但是由于**input_**是Rcpp::NumericVector,并且我正在使用指针初始化arma::cube变量,这会有影响吗?虽然我可能是错的。 - master_goon
1个回答

5

在新版本的Armadillo (5.300)之后进行编辑

在StackOverflow上进行了最初的问答之后,Conrad Sanderson和我就这个问题进行了一些邮件讨论。按照设计,arma::cube对象为每个切片(第三个维度)创建一个arma::mat。即使从现有内存中复制数据(如原始问题中所述),在创建cube时也会执行此操作。由于这并不总是需要的,我建议应该有一个选项来禁用对切片进行矩阵预分配。截至目前的Armadillo版本(5.300.4),现在已经有了这个选项。可以从CRAN安装。

示例代码:

library(RcppArmadillo)
library(inline)
code <- "
  Rcpp::NumericVector input_(input);
  arma::cube disturb(input_.begin(), 2, 2, 50000000, false, true, false);
  return wrap(2);
"
Test <- cxxfunction(signature(input = "numeric"), plugin = "RcppArmadillo", body = code)
input <- array(rnorm(2 * 2 * 50000000), dim = c(2, 2, 50000000))
Test(input)

关键在于现在使用arma::cube disturb(input.begin(), 2, 2, 50000000, false, true, false);调用了cube构造函数。这里的最后一个false是新的prealloc_mat参数,它确定是否预分配矩阵。在没有预分配矩阵的情况下,slice方法仍然可以正常工作 - 矩阵将按需分配。但是,如果您直接访问cubemat_ptrs成员,则会填充NULL指针。帮助也已更新。 非常感谢Conrad Sanderson迅速提供了这个额外选项,并感谢Dirk Eddelbuettel在Rcpp和RcppArmadillo上的所有工作! 原始答案 这有点奇怪。我尝试了各种不同的数组大小,问题只出现在第三个维度远大于其他两个维度的数组中。以下是一个可重现的示例:
library("RcppArmadillo")
library("inline")
code <- "
Rcpp::NumericVector input_(input);
IntegerVector dim = input_.attr(\"dim\");
arma::cube disturb(input_.begin(), dim[0], dim[1], dim[2], false);
disturb[0, 0, 0] = 45;
return wrap(2);
"
Test <- cxxfunction(signature(input = "numeric"), plugin = "RcppArmadillo", body = code)
input <- array(0, c(1e7, 2, 2))
Test(input)
# no change in memory usage

dim(input) <- c(2, 1e7, 2)
gc()
Test(input)
# no change in memory usage

dim(input) <- c(2, 2, 1e7)
gc()
Test(input)
# spike in memory usage

dim(input) <- c(20, 2, 1e6)
gc()
Test(input)
# no change in memory usage

这表明与Aramadillo库的实现方式有关(或可能与RcppArmadillo有关)。它肯定不是你做错了什么。请注意,我已经进行了一些修改来代替数据(将第一个元素设置为45),并且您可以确认在每种情况下数据确实是就地修改的,这表明没有发生复制。目前,我建议如果可能的话,组织您的3D数组,使最大的维度不是第三个维度。编辑后,经过更深入的挖掘,看起来在创建arma::cube时确实分配了RAM。在中,在create_mat方法中,有以下代码:
if(n_slices <= Cube_prealloc::mat_ptrs_size)
  {
  access::rw(mat_ptrs) = const_cast< const Mat<eT>** >(mat_ptrs_local);
  }
else
  {
  access::rw(mat_ptrs) = new(std::nothrow) const Mat<eT>*[n_slices];

  arma_check_bad_alloc( (mat_ptrs == 0), "Cube::create_mat(): out of memory" );
  }
}

Cube_prealloc::mat_ptrs_size 看起来是4,因此对于任何具有超过4个切片的数组,这实际上都是一个问题。

我在github上发布了一个问题

EDIT2 然而,这绝对是底层Armadillo代码的问题。以下是一个可重现的例子,完全不使用Rcpp。这仅适用于Linux-它使用来自如何在c++中获取运行时内存使用情况?的代码来提取正在运行的进程的当前内存使用情况。

#include <iostream>
#include <armadillo>
#include <unistd.h>
#include <ios>
#include <fstream>
#include <string>

//////////////////////////////////////////////////////////////////////////////
//
// process_mem_usage(double &, double &) - takes two doubles by reference,
// attempts to read the system-dependent data for a process' virtual memory
// size and resident set size, and return the results in KB.
//
// On failure, returns 0.0, 0.0

void process_mem_usage(double& vm_usage, double& resident_set)
{
   using std::ios_base;
   using std::ifstream;
   using std::string;

   vm_usage     = 0.0;
   resident_set = 0.0;

   // 'file' stat seems to give the most reliable results
   //
   ifstream stat_stream("/proc/self/stat",ios_base::in);

   // dummy vars for leading entries in stat that we don't care about
   //
   string pid, comm, state, ppid, pgrp, session, tty_nr;
   string tpgid, flags, minflt, cminflt, majflt, cmajflt;
   string utime, stime, cutime, cstime, priority, nice;
   string O, itrealvalue, starttime;

   // the two fields we want
   //
   unsigned long vsize;
   long rss;

   stat_stream >> pid >> comm >> state >> ppid >> pgrp >> session >> tty_nr
               >> tpgid >> flags >> minflt >> cminflt >> majflt >> cmajflt
               >> utime >> stime >> cutime >> cstime >> priority >> nice
               >> O >> itrealvalue >> starttime >> vsize >> rss; // don't care about the rest

   stat_stream.close();

   long page_size_kb = sysconf(_SC_PAGE_SIZE) / 1024; // in case x86-64 is configured to use 2MB pages
   vm_usage     = vsize / 1024.0;
   resident_set = rss * page_size_kb;
}

using namespace std;
using namespace arma;

void test_cube(double* numvec, int dim1, int dim2, int dim3) {
  double vm, rss;

  cout << "Press enter to continue";
  cin.get();

  process_mem_usage(vm, rss);
  cout << "Before:- VM: " << vm << "; RSS: " << rss << endl;

  cout << "cube c1(numvec, " << dim1 << ", " << dim2 << ", " << dim3 << ", false)" << endl;
  cube c1(numvec, dim1, dim2, dim3, false);

  process_mem_usage(vm, rss);
  cout << "After:-  VM: " << vm << "; RSS: " << rss << endl << endl;
}

int
main(int argc, char** argv)
  {
  double* numvec = new double[40000000];

  test_cube(numvec, 10000000, 2, 2);
  test_cube(numvec, 2, 10000000, 2);
  test_cube(numvec, 2, 2, 1000000);
  test_cube(numvec, 2, 2, 2000000);
  test_cube(numvec, 4, 2, 2000000);
  test_cube(numvec, 2, 4, 2000000);
  test_cube(numvec, 4, 4, 2000000);
  test_cube(numvec, 2, 2, 10000000);

  cout << "Press enter to finish";
  cin.get();

  return 0;
  }
编辑3 根据上面的 create_mat 代码,一个arma::mat被创建用于每一个立方体的切片。在我的64位机器上,这会导致每个切片有184字节的开销。对于一个有5e7个切片的立方体来说,这相当于8.6 GiB的开销,尽管底层的数值数据只占用了1.5 GiB。我已经给Conrad Sanderson发送了电子邮件询问这是否是Armadillo工作方式的基本特性或者是否可以更改,但目前看来,如果可能的话,你希望你的 slice 维度(第三个维度)是三个维度中最小的。值得注意的是,这适用于所有cube,而不仅仅适用于从现有内存创建的那些。使用 arma::cube(dim1, dim2, dim3) 构造函数会导致相同的内存使用情况。

1
刚在RcppArmadillo github页面上发布了一个问题。 - Nick Kennedy
1
这是Armadillo本身的问题,而非RcppArmadillo的问题。我会让Conrad Sanderson知道。 - Nick Kennedy
1
@DirkEddelbuettel 谢谢,同时也很抱歉之前曾经误以为是你的包代码出了问题! - Nick Kennedy
3
那是一个公正的推测。我通常会先想这是我的错。如果你结婚时间像我这样长的话,情况就是这样了 ;-) - Dirk Eddelbuettel
@DirkEddelbuettel 现在已经更新了 Armadillo 5.300 中的新 prealloc_mat 选项。 - Nick Kennedy
是的,最终版本的Armadillo 5.300(现在是5.300.4)昨天非常晚才发布,我正在准备它。我们在GitHub上有几个预发布版本。 - Dirk Eddelbuettel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接