使用 Rcpp 就地编辑后求和函数的问题

problem with sum function after inplace editing using Rcpp

如果在 Rcpp 中修改 IntegerVector 的值:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
void test(IntegerVector x) {
  x[5] = 77;
}

在 运行 test() R 中的函数之后:

x <- 10:1
test(x)
print(x)  #  10  9  8  7  6 77  4  3  2  1
sum(x)  # 55

求和函数return原始数组10:1的值。 我怎么解决这个问题?

使用e.g.时没有问题x <- sample(10L) 相反。

@F.Privé的猜测是正确的。这是 ALTREP 的问题,Rcpp 尚不支持,c.f。 Rcpp/#812 and Rcpp/#906。我们可以通过检查变量 x:

更明确地看到这一点
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
void test(IntegerVector x) {
  x[5] = 77;
}

/*** R
x <- 10:1
.Internal(inspect(x))
test(x)
.Internal(inspect(x))
print(x)  #  10  9  8  7  6 77  4  3  2  1
sum(x)  # 55

x <- 10:1
.Internal(inspect(x))
x[6] <- 77L
.Internal(inspect(x))
print(x)  #  10  9  8  7  6 77  4  3  2  1
sum(x)
*/

第一个块给出:

> x <- 10:1

> .Internal(inspect(x))
@55f79a9d6c58 13 INTSXP g0c0 [NAM(3)]  10 : 1 (compact)

> test(x)

> .Internal(inspect(x))
@55f79a9d6c58 13 INTSXP g0c0 [NAM(3)]  10 : 1 (expanded)

> print(x)  #  10  9  8  7  6 77  4  3  2  1
 [1] 10  9  8  7  6 77  4  3  2  1

> sum(x) # 55
[1] 55

而第二个块给出:

> x <- 10:1

> .Internal(inspect(x))
@55f79b1f9018 13 INTSXP g0c0 [NAM(3)]  10 : 1 (compact)

> x[6] <- 77L

> .Internal(inspect(x))
@55f7a096e5e8 13 INTSXP g0c4 [NAM(1)] (len=10, tl=0) 10,9,8,7,6,...

> print(x)  #  10  9  8  7  6 77  4  3  2  1
 [1] 10  9  8  7  6 77  4  3  2  1

> sum(x)
[1] 127

所以在vector中改变一个值后,它仍然声称是10 : 1,为此sum使用了一个捷径。有关 ALTREP 的进一步阅读(包括参考),请参阅 here

目前唯一的解决办法似乎是避免改变函数参数。

这是我发布的 elsewhere 相关内容:

所以这里有几件事,@ltierney and/or @kalibera 提到了其中的大部分,但也许可以从更具体的可能编码模式中受益。

这里问题的症结在于通过写入其 DATAPTR 寻址内存的元素来内联修改 SEXP 的有效负载。是否可以做(是),如果没有 confirming/being 保证可以做就安全吗(不,永远不会)。对于在同一 C/C++ 代码块中创建的对象,您可能有这样的先验保证,但您不会为从 R.

传递下来的对象提供这样的保证

具体来说,我不知道在任何时候写入由 INTEGER() 编辑的指针 return 在 R-[=56= 中的 SEXP 上是可以的] 没有先检查 MAYBE_SHARED()。如果 MAYBE_SHARED(x) return 为假,那么没关系,您可以继续写入指针,就像您的代码所做的那样。

如果MAYBE_SHARED(x) == TRUE,则需要复制,对副本进行操作,然后return那个。当你在 C/C++ 代码中,在获取指向事物的数据指针的级别时,你的代码需要明确地导致重复,保护新的重复结果等。

现在发生这种特殊情况的原因是在紧凑序列的情况下,除非 R 本身是以特定的非默认方式构建的,否则紧凑序列总是有 NAMED(x) == MAXNAMED(即 2),从创作点。正如卢克指出的那样,这可能会改变,但目前是设计使然。因此,即使在闭包不强制 NAMED 计数的 .Call 情况下,紧凑序列在内联修改之前也始终需要复制。虽然在紧凑序列的情况下我们可以做出不同的选择,但 Luke 关于其他 ALTREP 的观点更为重要。

可能存在 ALTREP SEXP,其中指针 return 所指向的内存,例如 INTEGER 由于某种原因实际上是不可写的内存。那些 ALTREP 类 将声明的方式是通过做与紧凑序列相同的事情,通过将自己标记为 "IMMUTABLE",即通过做 MARK_NOT_MUTABLE(x)(当前设置 NAMEDMAXNAMED,但它是面向未来的,不会最终改变引用计数)。这声明了在任何获取数据指针并写入数据指针的代码之前必须复制 SEXP 的契约。

最后,我同意这是一个非常奇怪的意外行为,但这是由于未能满足一直存在的合同。在过去的某些情况下 ignore/be 松懈可能是安全的(ish,我仍然怀疑),但随着 ALTREP 的出现,现在必须始终遵循此线程中概述的原因。

因此,所有要从预先存在的(R 级)SEXP 中获取数据指针并写入其中的所有代码都必须遵循以下模式(或等效地注意):

SEXP awesomefun(SEXP x) 
{
    int nprot = 0;
    if(MAYBE_SHARED(x)) {
        PROTECT(x = duplicate(x)); nprot++; 
    }
    /* do awesome things to x that modify it inline
        protect other things as necessary but always increment nprot when you do, 
        decrement nprot if you ever unprotect */
    if(nprot) UNPROTECT(nprot);
    return x;
}

任何写入从 SEXP 检索到的数据指针的代码,它本身并没有创建(即从 R 端下来的任何东西)而不这样做已经违反了 C-API 合同,但现在也是正如激励性示例所示,ALTREP 是致命的不安全。

而且,再一次,请记住,紧凑序列的行为可能有所不同,因此该代码恰好可以工作,但其他 ALTREP 类(Luke 提到内存映射文件支持的 ALTREPS)不能,所以紧凑序列的行为实际上不是这里的问题。

希望对您有所帮助,让事情变得更清晰。

最佳