查看导致segfault的cpp代码

Bra*_*sen 1 r rcpp

我有一些在R函数中运行的cpp代码,调用大约80k次.它的测试套件是全面的并且通过.它的前60k次似乎运行良好,然后在中间的某个地方,我得到了一个段错误.

*** Error in `/usr/lib/R/bin/exec/R': malloc(): memory corruption: 0x00000000047150f0 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x77725)[0x7f684462e725]
/lib/x86_64-linux-gnu/libc.so.6(+0x819be)[0x7f68446389be]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f684463a5a4]
/usr/lib/R/lib/libR.so(Rf_allocVector3+0x70d)[0x7f6844cd617d]
... # more
Run Code Online (Sandbox Code Playgroud)

以下是我的一些代码示例,你能看到它有什么问题吗?

返回一个LogicalVector(例如TRUE/ FALSEvector),其中前导NAs被标记为TRUE

#include <Rcpp.h>

using namespace Rcpp;

// [[Rcpp::export]]
LogicalVector leading_na(IntegerVector x) {
  int n = x.size();
  LogicalVector leading_na(n);

  int i = 0;
  while(x[i] == NA_INTEGER) {
    leading_na[i] = TRUE;
    i++;
  }
  return leading_na;
}
Run Code Online (Sandbox Code Playgroud)

返回一个LogicalVector(例如TRUE/ FALSEvector),其中尾随NAs被标记为TRUE

// [[Rcpp::export]]
LogicalVector trailing_na(IntegerVector x) {
  LogicalVector trailing_na = leading_na(rev(x));
  return rev(trailing_na);
}
Run Code Online (Sandbox Code Playgroud)

复制na.locf(x, na.rm = TRUE)zoo包中的功能:

// [[Rcpp::export]]
IntegerVector na_locf(IntegerVector x) {
  int n = x.size();
  LogicalVector lna = leading_na(x);

  for(int i = 0; i<n; i++) {
    if((i > 0) & (x[i] == NA_INTEGER) & (lna[i] != TRUE)) {
        x[i] = x[i-1];
      }
  }
  return x;
}
Run Code Online (Sandbox Code Playgroud)

返回有一个数字的向量中的最后一个位置:

// [[Rcpp::export]]
int max_x_pos(IntegerVector x) {
  IntegerVector y = rev(x);
  int n = x.size();
  LogicalVector leading_na(n);

  int i = 0;
  while(y[i] == NA_INTEGER) {
    i++;
  }

  return n-i;
}
Run Code Online (Sandbox Code Playgroud)

nru*_*ell 6

为了解决主要问题,您将获得看似随机的段错误,因为您的代码包含未定义的行为 - 特别是数组边界违规.既然你之前已经注意到你是C++的新手,那么你至少应该仔细阅读这个讨论这个特殊错误的问题的第一个答案.对于那些从其他语言转向C或C++的人来说,UB可能是一个很滑的概念,主要是因为它并不总是以错误的形式表现出来.行为实际上是 未定义的,因此无法知道结果是什么,也不应期望行为在平台,编译器甚至编译器版本之间保持一致.

我将使用您的leading_na函数来演示,但该max_x_pos函数具有相同的问题:

// [[Rcpp::export]]
Rcpp::LogicalVector leading_na(Rcpp::IntegerVector x) {
    int n = x.size();
    Rcpp::LogicalVector leading_na(n);

    int i = 0;
    while (x[i] == NA_INTEGER) {
        // ^^^^  
        Rcpp::Rcout << i << "\n";

        leading_na[i] = TRUE;
        i++;
    }

    return leading_na;
} 
Run Code Online (Sandbox Code Playgroud)

由于没有强制约束的任何内容i < n,当x仅包含NA元素时,代码继续评估x[n](以及可能的后续索引),这是未定义的.但是,对于较小的向量,这在我的机器上运行得很好(我最终设法使其在更大的输入时崩溃),这正是为什么难以识别与UB相关的错误的原因:

leading_na(rep(NA, 5))
# 0
# 1
# 2
# 3
# 4
# [1] TRUE TRUE TRUE TRUE TRUE 
Run Code Online (Sandbox Code Playgroud)

但是,当我们operator[]at()成员函数替换它,它执行相同的元素访问,但也在运行时进行边界检查时,错误很明显:

// [[Rcpp::export]]
Rcpp::LogicalVector leading_na2(Rcpp::IntegerVector x) {
    int n = x.size();
    Rcpp::LogicalVector leading_na(n);

    int i = 0;
    while (x.at(i) == NA_INTEGER) {
        Rcpp::Rcout << i << "\n";

        leading_na[i] = TRUE;
        i++;
    }

    return leading_na;
}
Run Code Online (Sandbox Code Playgroud)

然后

leading_na2(rep(NA, 5))
# 0
# 1
# 2
# 3
# 4
# Error: index out of bounds 
Run Code Online (Sandbox Code Playgroud)

请注意,提供的附加边界检查at 确实会产生轻微的性能成本,因为此检查在运行时发生,因此即使在开发阶段使用at而不是operator[]在开发阶段使用,一旦您的代码经过全面测试,它就是一个好主意.operator[]假设需要更好的性能,可能是一个好主意.


至于解决方案,第一个是保持while循环,只需添加一个值的检查i:

while (i < n && x[i] == NA_INTEGER) {
    leading_na[i] = TRUE;
    i++;
} 
Run Code Online (Sandbox Code Playgroud)

请注意,我写的i < n && x[i] == NA_INTEGER x[i] == NA_INTEGER && i < n.由于&&执行短路评估,当i < n评估为false在第一个版本中,表达x[i] == NA_INTEGER计算-这是很好的,因为我们已经看到,这是不确定的行为.

另一个选择是使用一个for循环代替,根据我的经验,至少可以更好地"提醒"我们检查我们的界限:

for (int i = 0; i < n && x[i] == NA_INTEGER; i++) {
    leading_na[i] = TRUE;
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,使用while循环或for循环的选择并不重要,只要您选择的是正确编写的.

另一个选项(或两个)是使用迭代器而不是索引,在这种情况下,您可以使用while循环或for循环:

// [[Rcpp::export]]
Rcpp::LogicalVector leading_na5(Rcpp::IntegerVector x) {
    int n = x.size();
    Rcpp::LogicalVector leading_na(n);

    Rcpp::IntegerVector::const_iterator it_x = x.begin();
    Rcpp::LogicalVector::iterator first = leading_na.begin(),
        last = leading_na.end();

/*
    while (first != last && *it_x++ == NA_INTEGER) {
        *first++ = TRUE;
    }
*/

    for ( ; first != last && *it_x == NA_INTEGER; ++first, ++it_x) {
        *first = TRUE;
    }

    return leading_na;
} 
Run Code Online (Sandbox Code Playgroud)

虽然迭代器是非常有用的设备,但我不确定它们在这种特殊情况下是否比手动索引提供任何好处,因此我建议使用前两种方法之一.


与段错误无关,您的代码还有一些其他方面值得解决.

  1. 在R,&&||执行原子逻辑与和逻辑原子OR,分别,同时&|分别执行矢量化逻辑与和矢量化逻辑OR.在C++中,&&||表现为它们中的R做,但&|是(原子)按位 AND和(原子)按位分别OR,.只是偶然,使用&&&在上面的函数中使用具有相同的效果,但是你需要修复它,因为你的意图是使用逻辑运算,而不是按位对应.
  2. 这是更具体的RCPP/R的C API,不过虽然用x[i] == NA_INTEGER呢,其实,如果测试x[i]NA,并非所有类型的这样的表现.IIRC,对任何平等的测试NA_REAL都是错误的,甚至是错误的NA_REAL == NA_REAL; 对于非整数算术类型(数字和复数(REALSXP/ CPLXSXP)),您很可能也想检查值是否为NaN.Rcpp根据对象类型提供了一些不同的方法.对于任何存储类型的向量,Rcpp::is_na(x)将返回与大小相同的逻辑向量x.对于原子值,我通常使用Rcpp::traits::is_na<SEXPTYPE>(x[i])- REALSXPfor double,INTSXPfor int,CPLXSXPfor Rcomplex等.但是,我认为你可以等效地使用向量的相应静态成员函数,例如Rcpp::NumericVector::is_na(x[i]),等等,在这种情况下你不需要记住各种SEXPTYPEs.
  3. 严格来说,没有TRUEFALSE在C++或C中; 这些(可能是)R的API提供的便利类型定义,所以请注意它们不存在于R的后端之外.当然,可以随意在你的Rcpp代码中使用它们,因为它们显然符合预期,但大多数人都遵守标准true,false甚至在使用Rcpp时也是如此.
  4. 一种挑剔,但你的leading_na函数声明了一个也被命名的局部变量leading_na,这有点令人困惑,或者至少是非正统的.
  5. 在处理对象大小时,请考虑使用std::size_t(标准C++)或R_xlen_t(R API特定),例如在此表达式中:int n = x.size();.那些是无符号整数类型,它应该足够大以存储任何对象的长度,其中int符号整数类型可能是也可能不够(通常是).99.9%的时间会发生最坏的情况是在使用ints而不是表达式中的其他两个时,你会得到一些额外的编译器警告(而不是错误)for (int i = 0; i < x.size(); i++) { // whatever }.在极少数情况下,可能会有更糟糕的反响,例如有符号整数溢出(这也是未定义的行为),所以请注意这种远程可能性.

这种答案变成了代码审查/肥皂盒咆哮,但希望你在那里找到一些有用的信息.

  • 很好的讨论 - + 1'. (2认同)