Просмотрите код cpp, вызывающий segfault

У меня есть некоторый код cpp, который выполняется внутри функции R, которая вызывается около 80 тысяч раз. Это набор тестов, полный и проходящий. Кажется, он работает нормально первые 60 000 раз, когда он вызывается, а затем где-то посередине я получаю сегфо.

*** 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 вектор) где ведущий 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;
}

Вернуть LogicalVector (например TRUE/FALSE вектор) где трейлинг NAs помечены как TRUE

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

Копирует функциональность 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 ответ

Решение

Чтобы решить основную проблему, вы получаете, казалось бы, случайные ошибки, потому что ваш код содержит неопределенное поведение - в частности, нарушение границы массива. Поскольку ранее вы отмечали, что вы довольно плохо знакомы с C++, вам стоит прочитать хотя бы первый ответ на этот вопрос, в котором обсуждается эта конкретная ошибка. UB может быть скользкой концепцией, чтобы обернуть голову людям, приходящим на C или C++ с других языков, в первую очередь потому, что он не всегда проявляется в форме ошибки. Поведение буквально не определено, поэтому нет никакого способа узнать, каков будет результат, и при этом вы не должны ожидать, что поведение будет согласованным на разных платформах, компиляторах или даже версиях компилятора.

Я буду использовать ваш 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() Функция-член, которая выполняет тот же элемент доступа, но также выполняет проверку границ во время выполнения, ошибка очевидна:

// [[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 не оценивается - что хорошо, потому что, как мы видели, это неопределенное поведение.

Другой вариант будет использоватьforцикл, который, как правило, лучше "напоминает" нам проверять наши границы, по моему опыту, по крайней мере:

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

Хотя итераторы являются очень полезными устройствами, я не уверен, что они обеспечивают какую-либо выгоду по сравнению с ручной индексацией в данном конкретном случае, поэтому я бы рекомендовал использовать один из первых двух подходов.


Не связанные с segfault, есть несколько других аспектов вашего кода, которые стоит рассмотреть.

  1. В R, && а также || выполнять атомное логическое И и атомное логическое ИЛИ, соответственно, при этом & а также | выполнить векторизованное логическое И и векторизованное логическое ИЛИ соответственно. В C++ && а также || вести себя так же, как в R, но &а также| являются (атомарными) побитовым И и (атомарным) побитовым ИЛИ, соответственно. Просто случайно, используя &имел тот же эффект, что и при использовании&&в вашей функции выше, но вы захотите исправить это, поскольку вы намеревались использовать логическую операцию, а не побитовый аналог.
  2. Это более конкретно для C API Rcpp / R, но хотя и использует 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])- REALSXP за double, INTSXP за int, CPLXSXPза Rcomplex, и так далее. Тем не менее, я думаю, что вы можете эквивалентно использовать соответствующую статическую функцию-член вектора, например Rcpp::NumericVector::is_na(x[i])и т. д., в этом случае вам не нужно запоминать различныеSEXPTYPEs.
  3. Собственно говоря нет TRUE или же FALSE в C++ или C; это (предположительно) удобные определения типов, предоставляемые API R, так что просто имейте в виду, что они не существуют вне бэкэнда 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 }, В редких случаях возможны худшие последствия, такие как целочисленное переполнение со знаком (что также является неопределенным поведением), поэтому просто помните об этой удаленной возможности.

Этот ответ в некотором роде превратился в скандал с обзором кода / soapbox, но, надеюсь, вы найдете там полезную информацию.

Другие вопросы по тегам