在R中使用.Call内部的结果不一致

时间:2016-08-05 14:40:55

标签: r rcpp

我使用Rcpp包的Rcpp.package.skeleton函数创建了一个包,其中包含一个.cpp文件,其中C ++函数返回0:

#include <Rcpp.h>

using namespace Rcpp;

// [[Rcpp::export]]
RcppExport SEXP just_zero() {
BEGIN_RCPP
    Rcpp::RNGScope __rngScope;
    return wrap(0.0);
END_RCPP
}

当我从R安装并加载包时,我可以通过lapply使用.Call调用该函数。正如所料,它(似乎)总是返回0:

> x <- lapply(seq(10000), function(i) { x <- .Call('just_zero'); stopifnot(x == 0); x } )
#*no errors!*

但是,显然lapply返回的值包含非零值:

> range(simplify2array(x))
[1] 0 3

不幸的是,使用set.seed并不能使这些返回值重现,有时我会得到[1] 0 0,有时会得到其他值,例如[1] "0" "TRUE"。另一条线索是删除行Rcpp::RNGScope __rngScope;可以解决问题。

为什么lapply返回的对象中存在非零元素(特别是当我们检查.Call返回的值时),以及如何使用RNGScope造成它?

我已经在Linux和OS X上重现了这种行为。来自OS X的会话信息粘贴在下面:

> sessionInfo()
R Under development (unstable) (2016-08-03 r71023)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: OS X Mavericks 10.9.5

locale:
[1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8

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

other attached packages:
[1] bug_1.0         devtools_1.12.0

loaded via a namespace (and not attached):
[1] tools_3.4.0   withr_1.0.2   memoise_1.0.0 Rcpp_0.12.6   git2r_0.15.0
[6] digest_0.6.10

2 个答案:

答案 0 :(得分:4)

这是一个为我一致地再现问题的例子。

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
void dummy() {}

extern "C" SEXP just_zero() {
  Rcpp::RNGScope rngScope;
  return wrap(0.0);
}

/*** R
n <- 1E5
x <- unlist(lapply(seq(n), function(i) {
  .Call('just_zero')
}))

unique(x)
*/

在此上调用Rcpp::sourceCpp()就可以了。例如

> unique(x)
 [1]     0  8371 16021 20573 25109 43103 47563 56438 60852 78413 82773 95765

导致此问题的原因是什么?要理解这一点,我们需要了解RNGScope的定义:我们可以在这里看到它:

https://github.com/RcppCore/Rcpp/blob/9fbdf78fe225607dc2c8af7267a6840af0aba10e/inst/include/Rcpp/stats/random/random.h#L27-L31

及其使用的方法在此处定义:

https://github.com/RcppCore/Rcpp/blob/9fbdf78fe225607dc2c8af7267a6840af0aba10e/src/api.cpp#L63-L75

这里定义了最重要的函数PutRNGState

https://github.com/wch/r-source/blob/1c88a057594a0348f2bf75514a8015caeedbff93/src/main/RNG.c#L424-L446

现在,这实际上是在调用just_zero时发生的情况:

  1. RNGScope对象已创建,其关联的析构函数已准备好运行。
  2. 创建wrap(0.0)的结果,即REALSXP长度为1且值为0的结果。请注意,此对象不受保护,因此符合垃圾回收条件。
  3. 函数返回,并调用RNGScope析构函数。
  4. 这将调用R例程PutRNGstate(),它本身将调用allocVector,从而触发垃圾收集器。这意味着可以收集您想要返回的SEXP对象,因此将是垃圾!
  5. 所以,总而言之 - 使用Rcpp属性,因为它会安全地为你完成这一切。

    要了解为什么Rcpp属性使这个安全&#39;,请查看生成的代码:

    // just_zero
    SEXP just_zero();
    RcppExport SEXP sourceCpp_0_just_zero() {
    BEGIN_RCPP
        Rcpp::RObject __result;
        Rcpp::RNGScope __rngScope;
        __result = Rcpp::wrap(just_zero());
        return __result;
    END_RCPP
    }
    

    请注意,输出结果已分配给保护对象的Rcpp::RObject,我们确保在 RNGScope对象之前构造此对象,这样可以确保在RNGScope析构函数运行时保持受保护。

答案 1 :(得分:3)

这不是一个完全答案,但我会随着它的扩展而扩展它。但是,我无法在macOS上重现此行为。

我相信您遇到的问题是由于您使用RcppExport和Rcpp属性,例如:

// [[Rcpp::export]]
RcppExport SEXP just_zero()

应该通过以下方式完成:

library("Rcpp")
cppFunction("double just_zero() {
    return 0.0;
}")

x <- lapply(seq(10000), function(i) { x <- just_zero(); stopifnot(x == 0); x } )

all(range(simplify2array(x)) == 0)

或将以下内容放入file.cpp

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
double just_zero() {
    return 0.0;
}

/*** R
x <- lapply(seq(10000), function(i) { x <- just_zero(); stopifnot(x == 0); x } )
all(range(simplify2array(x)) == 0)
*/

如果打算使用sourceCpp()