查看导致段错误的 cpp 代码
Review cpp code causing segfault
我在 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
这是我的一些代码示例,您能看出其中有什么问题吗?
Return a 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;
}
Return 一个 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;
}
Return向量中有数字的最后一个位置:
// [[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;
}
为了解决主要问题,您会收到看似随机的段错误,因为您的代码包含未定义的行为——特别是数组边界违规。由于您之前指出您是 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
提供的额外边界检查 会带来轻微的性能成本,因为此检查发生在运行时,因此即使它可能是在开发阶段使用 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
是 not 评估-- 这很好,因为正如我们所见,这是未定义的行为。
另一种选择是使用 for
循环,这往往会更好地 "reminding" 我们检查我们的边界,根据我的经验,至少:
for (int i = 0; i < n && x[i] == NA_INTEGER; i++) {
leading_na[i] = TRUE;
}
在这种情况下,选择使用 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;
}
虽然迭代器是非常有用的设备,但我不确定在这种特殊情况下它们是否比手动索引有任何好处,因此我建议使用前两种方法中的一种。
与段错误无关,您的代码还有其他一些方面值得解决。
- 在R中,
&&
和||
分别执行原子逻辑与和原子逻辑或,而&
和|
执行矢量化逻辑与和矢量化逻辑或, 分别。在 C++ 中,&&
和 ||
的行为与它们在 R 中的行为相同,但 &
和 |
是(原子的)按位 AND 和(原子)按位 或,分别。偶然地,使用 &
与在上面的函数中使用 &&
具有相同的效果,但您需要修复它,因为您的意图是使用逻辑运算,而不是按位运算。
- 这更特定于 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)
将 return 与 x
大小相同的逻辑向量。对于原子值,我通常使用 Rcpp::traits::is_na<SEXPTYPE>(x[i])
-- REALSXP
表示 double
,INTSXP
表示 int
,CPLXSXP
表示 Rcomplex
,等等。但是,我认为您可以等效地使用向量的相应静态成员函数,例如Rcpp::NumericVector::is_na(x[i])
,等等,在这种情况下你不需要记住各种SEXPTYPE
。
- 严格来说,C++ 或 C 中没有
TRUE
或 FALSE
;这些(大概)是 R 的 API 提供的便利类型定义,所以请注意它们不存在于 R 的后端之外。当然,您可以在 Rcpp 代码中随意使用它们,因为它们的行为显然符合预期,但大多数人即使在使用 Rcpp 时也会坚持标准 true
和 false
。
- 有点吹毛求疵,但是您的
leading_na
函数声明了一个局部变量,也命名为 leading_na
,这有点令人困惑,或者至少是非正统的。
- 在处理对象大小时,考虑使用
std::size_t
(标准 C++)或 R_xlen_t
(R API 特定),例如在这个表达式中:int n = x.size();
。这些是 unsigned 整数类型,应该足够大以存储任何对象的长度,其中 int
是 signed 整数类型这可能足够也可能不足够(通常是)。 99.9% 的情况下,最糟糕的情况是在使用 int
而不是 for (int i = 0; i < x.size(); i++) { // whatever }
等表达式中的其他两个时,您会收到一些额外的编译器警告(不是错误)。在极少数情况下,可能会产生更严重的影响,例如有符号整数溢出(这也是未定义的行为),因此请注意这种可能性很小的情况。
这个答案有点变成了代码审查/肥皂盒咆哮,但希望你能从中找到一些有用的信息。
我在 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
这是我的一些代码示例,您能看出其中有什么问题吗?
Return a 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;
}
Return 一个 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;
}
Return向量中有数字的最后一个位置:
// [[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;
}
为了解决主要问题,您会收到看似随机的段错误,因为您的代码包含未定义的行为——特别是数组边界违规。由于您之前指出您是 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
提供的额外边界检查 会带来轻微的性能成本,因为此检查发生在运行时,因此即使它可能是在开发阶段使用 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
是 not 评估-- 这很好,因为正如我们所见,这是未定义的行为。
另一种选择是使用 for
循环,这往往会更好地 "reminding" 我们检查我们的边界,根据我的经验,至少:
for (int i = 0; i < n && x[i] == NA_INTEGER; i++) {
leading_na[i] = TRUE;
}
在这种情况下,选择使用 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;
}
虽然迭代器是非常有用的设备,但我不确定在这种特殊情况下它们是否比手动索引有任何好处,因此我建议使用前两种方法中的一种。
与段错误无关,您的代码还有其他一些方面值得解决。
- 在R中,
&&
和||
分别执行原子逻辑与和原子逻辑或,而&
和|
执行矢量化逻辑与和矢量化逻辑或, 分别。在 C++ 中,&&
和||
的行为与它们在 R 中的行为相同,但&
和|
是(原子的)按位 AND 和(原子)按位 或,分别。偶然地,使用&
与在上面的函数中使用&&
具有相同的效果,但您需要修复它,因为您的意图是使用逻辑运算,而不是按位运算。 - 这更特定于 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)
将 return 与x
大小相同的逻辑向量。对于原子值,我通常使用Rcpp::traits::is_na<SEXPTYPE>(x[i])
--REALSXP
表示double
,INTSXP
表示int
,CPLXSXP
表示Rcomplex
,等等。但是,我认为您可以等效地使用向量的相应静态成员函数,例如Rcpp::NumericVector::is_na(x[i])
,等等,在这种情况下你不需要记住各种SEXPTYPE
。 - 严格来说,C++ 或 C 中没有
TRUE
或FALSE
;这些(大概)是 R 的 API 提供的便利类型定义,所以请注意它们不存在于 R 的后端之外。当然,您可以在 Rcpp 代码中随意使用它们,因为它们的行为显然符合预期,但大多数人即使在使用 Rcpp 时也会坚持标准true
和false
。 - 有点吹毛求疵,但是您的
leading_na
函数声明了一个局部变量,也命名为leading_na
,这有点令人困惑,或者至少是非正统的。 - 在处理对象大小时,考虑使用
std::size_t
(标准 C++)或R_xlen_t
(R API 特定),例如在这个表达式中:int n = x.size();
。这些是 unsigned 整数类型,应该足够大以存储任何对象的长度,其中int
是 signed 整数类型这可能足够也可能不足够(通常是)。 99.9% 的情况下,最糟糕的情况是在使用int
而不是for (int i = 0; i < x.size(); i++) { // whatever }
等表达式中的其他两个时,您会收到一些额外的编译器警告(不是错误)。在极少数情况下,可能会产生更严重的影响,例如有符号整数溢出(这也是未定义的行为),因此请注意这种可能性很小的情况。
这个答案有点变成了代码审查/肥皂盒咆哮,但希望你能从中找到一些有用的信息。