Учитывая, что b всегда ненулевое, почему `b? --b: ++ b` работает, а `--b` - нет? - PullRequest
16 голосов
/ 17 июня 2011

Я пытался умножить два целых числа, используя рекурсию, и случайно написал этот код:

//the original version
int multiply(int a, int b)
{
  if ( !b )
     return 0;
  else
     return a + multiply(a, b ? --b : ++b ); //accident
}

Я сказал, что написал это случайно, потому что намеревался написать:

b > 0 ? --b : ++b вместо b ? --b : ++b

Я понимаю, что то, что я намеревался написать , не сработало бы . Но что меня удивляет, так это то, что я не намеревался написать действительно работает .

Теперь отмечу, что b ?--b : ++b в основном эквивалентен --b, потому что b в блоке else гарантированно будет ненулевым. Поэтому я изменил приведенный выше код, заменив b?--b:++b на --b, как показано ниже:

//the modified version
int multiply(int a, int b)
{
  if ( !b )
     return 0;
  else
     return a + multiply(a, --b); //modification here
}

Поскольку оригинальная версия работает, я ожидал, что модифицированная версия также будет работать. Но опять же, к моему удивлению, это не работает!

  • Что не так модифицированной версии?
  • Разве это не эквивалентно оригинальной версии?
  • Разве --b не эквивалентно b ?--b : ++b ЕСЛИ b не равно нулю? Если его эквивалент, то почему первый код работает, а второй нет?

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

Ответы [ 7 ]

6 голосов
/ 17 июня 2011

Это не работает. Я не знаю, что Ideone курит, этот код переполняет стек.

EDIT

Пробовал на gcc 4.6.0 - segfault (из-за переполнения стека). Однако, если вы включаете -O2 оптимизаций, то действительно "это работает". В заключение: это работает случайно, в зависимости от того, что компилятор делает за кулисами.

g++ -g -Wall -o f f.cpp # stack overflow
g++ -O2 -g -Wall -o f f.cpp # "works"
5 голосов
/ 17 июня 2011

Вывод кода ниже должен дать очень сильную подсказку;)

#include <iostream>

using namespace std;

int multiply(int a, int b)
{
  cout << "a = " << a << "\tb = " << b << std::endl;
  if ( !b )
     return 0;
  else
     return a + multiply(a, b ? --b : ++b );
}

int main() {
        cout << multiply(12, 7) << endl;
        cout << multiply(12, -7) << endl;
        cout << multiply(-12, -7) << endl;
        cout << multiply(-12, 7) << endl;
        return 0;
}

Мне кажется, что он получает ответ, пройдя долгий путь.

Редактировать: В ответ на комментарий Nawaz ниже, первый метод работает из-за капризов целой арифметики со знаком, дополненной двумя.Как я уже сказал, это займет много времени.Он включен, как указывали другие, из-за какой-то оптимизации компилятора или другой.Вы можете увидеть это в приведенном ниже коде для тестового ввода, ранее предоставленного:

int mul2(int a, int b)
{
    int rv = 0;
    while (b--) rv += a;
    return rv;
}

На самом деле это должно работать для любой пары целых чисел, но в некоторых случаях потребуется некоторое время для запуска (но я ожидаю, что вы нене интересует эффективность в любом случае).

Второй случай не работает, потому что ваш условный b > 0 ? --b : ++b по существу преобразует b в abs(b).То есть вы добавляете только 7 раз в вашем тестовом примере, даже если b = -7.Если вы хотите, чтобы он вел себя так, как я думаю, вы хотите, чтобы он вел себя, вам вместо этого нужно сделать что-то вроде:

int multiply(int a, int b)
{
  if ( !b )
     return 0;
  else if (b < 0)
     return -a + multiply(-a, -b-1);
  else
     return a + multiply(a, --b);
}

Редактировать 2: Попробуйте вместо этого:

short multiply(short a, short b)
{
  if ( !b )
     return 0;
  else
     return a + multiply(a, b ? --b : ++b );
}

и

short multiply(short a, short b)
{
  if ( !b )
     return 0;
  else
     return a + multiply(a, --b);
}

Обе эти функции компилируются, запускаются и возвращают один и тот же (правильный) результат с оптимизацией или без нее.Как уже отмечали другие, причина разницы во времени выполнения, которую вы видите, может быть объяснена только тем, как компилятор оптимизирует ваш код.Вам нужно будет проанализировать ассемблерный код двух методов, чтобы определить основную причину расхождений во времени.

5 голосов
/ 17 июня 2011

TL; версия DR: Причина, по которой b? --b: ++b печатает результат, а --b завершается неудачно с SIGXCPU, заключается в том, что ideone устанавливает ограничение по времени для переданного кода. Одна версия оптимизируется лучше и завершаетсяв разрешенное время.Другая версия дает точно такой же ответ, но вы не увидите этого с ideone, потому что он работает слишком медленно и прерывается по времени.


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

Результатом преобразования будет что-то вроде http://ideone.com/AeBYI который действительно дает правильный результат.При более высоких настройках оптимизации компилятор может вычислять результаты во время компиляции и сохранять константы в результирующем коде.

1 голос
/ 17 июня 2011

Из-за того, как работают числа дополнения 2, ваш код является «правильным» для положительных и отрицательных значений для b.Просто для отрицательных значений b любой рекурсивной версии нужен большой стек для работы.Поэтому всякий раз, когда компилятор выдает нерекурсивную версию, у вас есть рабочий код.Таким образом, все сводится к следующему: какое правило мой компилятор использует внутренне, чтобы определить, когда генерировать нерекурсивный код.Это зависит только от того, как был написан компилятор.

1 голос
/ 17 июня 2011

Действительно, это не имеет ничего общего с --b, но с вашим алгоритмом.

Если b <0, что вы ожидаете? Вы будете бесконечно зацикливаться и в итоге будете переполнены стеком. </p>

Вот почему вы сначала получаете правильный результат умножения (12, 7), но затем ваша программа завершается ошибкой при вызове умножения (12, -7).

0 голосов
/ 17 июня 2011

Обе формы multiply аварийно завершают работу в Visual Studio с переполнением стека, когда b отрицательно.

Итак, ответ таков: ни одна форма не верна. Вероятно, в gcc происходит то, что из-за некоторой причуды (не ошибка!) компилятор оптимизирует удаление хвостовой рекурсии в первом примере, но не во втором.


Как примечание: даже если вы измените его на b > 0 ? --b : ++b, вы все равно не умножаетесь на знак b (например, multiply(-1, -1) == -1)

0 голосов
/ 17 июня 2011

Но что меня удивляет, так это то, что я не собирался писать, работает.

Очевидно, что на уровне оптимизации 2 в g ++ компилятор правильно видит, что если b положительно, ваш код эквивалентен a * b. Также очевидно, что если b отрицательно, ваш код вызывает неопределенное поведение. Компилятор оптимизирует это неопределенное поведение путем обобщения a * b во всех случаях. Вот ассемблер от g ++ -O2 (i686-apple-darwin10-g ++ - 4.2.1):

.globl __Z8multiplyii
__Z8multiplyii:
LFB1477:
   pushq %rbp
LCFI0:
   movq  %rsp, %rbp
LCFI1:
   xorl  %eax, %eax
   testl %esi, %esi
   je L5
   movl  %esi, %eax
   imull %edi, %eax
L5:
   leave
   ret 

Я не доверяю оптимизаторам. IMO ответ компилятора на распознавание неопределенного поведения должен заключаться в неудачной компиляции, а не в оптимизации неопределенного поведения.

Редактировать

Вместо того, чтобы добавлять другой ответ в качестве другого ответа, я добавлю еще один ответ в качестве правки.

Спросите любого физика значение бесконечной суммы 1 + 2 + 3 + ... и они скажут вам, что это -1/12. (Например, см. http://math.ucr.edu/home/baez/qg-winter2004/zeta.pdf).. Этот обходной путь работает по аналогичной схеме с двойным дополнением арифметики. Решение, которое не предполагает обходного обхода отрицательных чисел:

int multiply (int a, int b) {
   if (b == 0) {
      return 0;
   }
   else if (b < 0) {
      return -multiply (a, -b);
   }
   else {
      return a + multiply(a, b-1);
   }
}

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

int multiplyu(int a, unsigned int b) {
   return (b == 0) ? 0 : a + multiplyu(a, b-1);
}

int multiply(int a, int b) {
   return (b < 0) ? -multiplyu (a, -b) : multiplyu (a, b); 
}
...