查看导致segfault的cpp代码

时间:2016-08-28 01:58:28

标签: 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

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

返回LogicalVector(例如TRUE / FALSE向量),其中前导NA被标记为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;
}

返回LogicalVector(例如TRUE / FALSE向量),其中尾随NA被标记为TRUE

// [[Rcpp::export]]
LogicalVector trailing_na(IntegerVector x) {
  LogicalVector trailing_na = leading_na(rev(x));
  return rev(trailing_na);
}

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

// [[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;
}

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

// [[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;
}

1 个答案:

答案 0 :(得分:6)

要解决主要问题,您将获得看似随机的段错误,因为您的代码包含未定义的行为 - 特别是数组边界违规。既然你之前已经注意到你是C ++的新手,那么你至少应该阅读第一个回答这个特定错误的答案to this question是值得的。对于那些从其他语言转向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;
} 

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

leading_na(rep(NA, 5))
# 0
# 1
# 2
# 3
# 4
# [1] TRUE TRUE TRUE TRUE TRUE 

但是,当我们用operator[]成员函数替换at()时,它会执行相同的元素访问,但在运行时会also does bounds checking,但错误很明显:

// [[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;
}

然后

leading_na2(rep(NA, 5))
# 0
# 1
# 2
# 3
# 4
# Error: index out of bounds 

请注意,at 提供的附加边界检查会产生轻微的性能损失,因为此检查会在运行时进行,因此尽管使用{最好是一个好主意在开发阶段{1}}而不是at,一旦您的代码经过全面测试,最好还原到operator[],假设需要更好的性能。

对于解决方案,第一个是保留operator[]循环,只需添加对while的值的检查:

i

请注意,我写了while (i < n && x[i] == NA_INTEGER) { leading_na[i] = TRUE; i++; } 不是 i < n && x[i] == NA_INTEGER 。由于x[i] == NA_INTEGER && i < n执行短路评估,当&&在第一个版本中评估为i < n时,表达式false 评估 - 其中是好的,因为正如我们所看到的那样,这是未定义的行为。

另一种选择是使用x[i] == NA_INTEGER循环,这样可以更好地提醒&#34;提醒&#34;根据我的经验,我们至少要检查我们的界限:

for

在这种情况下,选择使用for (int i = 0; i < n && x[i] == NA_INTEGER; i++) { leading_na[i] = TRUE; } 循环或while循环并不重要,前提是您选择的内容都是正确写入的。

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

for

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

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

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