Почему рекурсивная версия этой функции работает быстрее?

Вот простой класс для перебора многомерного числового диапазона:

#include <array>
#include <limits>

template <int N>
class NumericRange
{
public:
  //  typedef std::vector<double>::const_iterator const_iterator;
  NumericRange() {
    _lower.fill(std::numeric_limits<double>::quiet_NaN());
    _upper.fill(std::numeric_limits<double>::quiet_NaN());
    _delta.fill(std::numeric_limits<double>::quiet_NaN());
  }
  NumericRange(const std::array<double, N> & lower, const std::array<double, N> & upper, const std::array<double, N> & delta):
    _lower(lower), _upper(upper), _delta(delta) {
    _state.fill(std::numeric_limits<double>::quiet_NaN());
    _next_index_to_advance = 0;
  }

  const std::array<double, N> & get_state() const {
    return _state;
  }

  void start() {
    _state = _lower;
  }

  bool in_range(int index_to_advance = N-1) const {
    return ( _state[ index_to_advance ] - _upper[ index_to_advance ] ) < _delta[ index_to_advance ];
  }

  void advance(int index_to_advance = 0) {
    _state[ index_to_advance ] += _delta[ index_to_advance ];
    if ( ! in_range(index_to_advance) ) {
      if (index_to_advance < N-1) {
    // restart index_to_advance
    _state[index_to_advance] = _lower[index_to_advance];

    // carry
    index_to_advance;
    advance(index_to_advance+1);
      }
    }
  }

private:
  std::array<double, N> _lower, _upper, _delta, _state;
  int _next_index_to_advance;
};

int main() {
  std::array<double, 7> lower{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
  std::array<double, 7> upper{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
  std::array<double, 7> delta{0.03, 0.06, 0.03, 0.06, 0.03, 0.06, 0.03};

  NumericRange<7> nr(lower, upper, delta);
  int c = 0;
  for (nr.start(); nr.in_range(); nr.advance()) {
    const std::array<double, 7> & st = nr.get_state();
    ++c;
  }
  std::cout << "took " << c << " steps" << std::endl;

  return 0;
}

Когда я заменяю функцию advance нерекурсивным вариантом, время выполнения увеличивается:

void advance(int index_to_advance = 0) {
  bool carry;
  do {
    carry = false;
    _state[ index_to_advance ] += _delta[ index_to_advance ];
    if ( ! in_range(index_to_advance) ) {
      if (index_to_advance < N-1) {
    // restart index_to_advance
    _state[index_to_advance] = _lower[index_to_advance];

    // carry
    ++index_to_advance;
    carry = true;
    //    advance(index_to_advance);
      }
    }
  } while (carry);
}

Время выполнения было взято с использованием пользовательского времени unix с помощью команды time. Код был скомпилирован с использованием gcc-4.7 с параметрами -std=c++11 -O3 (но я думаю, что он должен работать с c++0x на gcc-4.6). Рекурсивная версия заняла 13 секунд, а итеративная — 30 секунд. Оба требуют одинакового количества вызовов advance для завершения (и если вы печатаете массив nr.get_state() внутри цикла for(ns.start()...), оба делают одно и то же).

Это веселая загадка! Помогите мне понять, почему рекурсия будет более эффективной/оптимизируемой.


person user    schedule 03.06.2012    source источник
comment
Попробуйте профилировать. valgrind --callgrind — хороший профайлер.   -  person n. 1.8e9-where's-my-share m.    schedule 03.06.2012
comment
Лично мне нравится gdb. Перерыв + обратная дорожка   -  person David Stone    schedule 03.06.2012
comment
Производительность, которую я вижу, непоследовательна. Для итеративной версии я получаю либо 30, либо 100 секунд на разных прогонах. Возможно, есть тонкая проблема с кэшированием.   -  person Vaughn Cato    schedule 03.06.2012


Ответы (2)


Рекурсивная версия является примером хвостовой рекурсии, что означает, что компилятор может преобразовать рекурсию в итерацию. Теперь, когда преобразование выполнено, рекурсивная функция будет выглядеть примерно так:

void advance(int index_to_advance = 0) {
    _state[ index_to_advance ] += _delta[ index_to_advance ];
    while ( !in_range(index_to_advance) && index_to_advance < N-1 ) {
        // restart index_to_advance
        _state[index_to_advance] = _lower[index_to_advance];

        // carry
        ++index_to_advance;
        _state[ index_to_advance ] += _delta[ index_to_advance ];
    }
  }

Как видите, ваша версия содержит один дополнительный тест и переменную условия. Цикл, если присмотреться, эквивалентен

for( ; index_to_advance < N-1 && !in_range(index_to_advance);++index_to_advance)

(удаление ++index_to_advance в конце), и у оптимизатора может быть больше шансов развернуть это.

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

person David Rodríguez - dribeas    schedule 03.06.2012
comment
Вы уверены, что версия TCO верна? Он так и не завершился для меня (~ 30 минут). Я не смотрел в это, хотя - person sehe; 03.06.2012
comment
@sehe: Вы правы, преобразование неверно, первая строка должна быть внутри цикла (т.е. должна применяться к каждой итерации)... - person David Rodríguez - dribeas; 03.06.2012

Просто добавим больше деталей к тому, что сказал Дэвид Родригес:

При оптимизации хвостовой рекурсии функция принимает вид:

 void advance(int index_to_advance = 0) {
  top:
  _state[ index_to_advance ] += _delta[ index_to_advance ];
  if ( ! in_range(index_to_advance) ) {
    if (index_to_advance < N-1) {
      // restart index_to_advance
      _state[index_to_advance] = _lower[index_to_advance];

      // carry
      ++index_to_advance;
      goto top;
    }
  }
}

и это действительно имеет ту же производительность, что и рекурсивная версия в моей системе (g++ 4.6.3 -std=c++0x)

person Vaughn Cato    schedule 03.06.2012
comment
+1 Мне так странно, что label...goto был бы более эффективным (вы можете делать вещи с label...goto, которые перемещаются между областью действия и заставляют компилятор прыгать), но я понимаю, почему в этом случае. Спасибо! - person user; 04.06.2012