Почему целочисленное деление округляется во многих языках сценариев?


В языках, которые я тестировал, - (x div y ) не равно -x div y; я тестировал // в Python, / в Ruby, div в Perl 6; C имеет аналогичное поведение.

Это поведение обычно соответствует спецификации, так как div обычно определяется как округление вниз результата деления , однако это не имеет большого смысла с арифметической точки зрения, так как это заставляет div вести себя по-разному в зависимости от знака, и это вызывает путаница, такая как этот пост о том, как это делается в Python.

Есть ли какое-то конкретное обоснование для этого проектного решения, или просто div определяется таким образом с нуля? По-видимому, Guido van Rossum использует аргумент когерентности в блоге, который объясняет, как это делается в Python, но вы также можете иметь когерентность, если решите округлить.

(вдохновленный этим вопросом PMurias в IRC-канале #perl6 )

5 42

5 ответов:

Операции с плавающей запятой определяются IEEE754 с учетом числовых приложений и по умолчанию округляются до ближайшего представимого значения очень строго определенным образом.

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

Другие ответы обсуждают связь с оператором остатка или модуля, которую язык может предоставить наряду с делением. К сожалению, у них все наоборот. Остаток зависит от определения деления, а не наоборот, в то время как модуль может быть определен независимо от деления - если оба аргумента оказываются положительными и деление округляет вниз, они работают, чтобы быть тем же самым, так что люди редко замечают.

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

В идеале мы хотели бы иметь две операции div и mod, удовлетворяющие для каждой b>0:

  1. (a div b) * b + (a mod b) = a
  2. 0 <= (a mod b) < b
  3. (-a) div b = -(a div b)

Это, однако, математическая невозможность. Если бы все вышесказанное было правдой, мы бы

1 div 2 = 0
1 mod 2 = 1

Так как это единственное целочисленное решение для (1) и (2). Следовательно, мы также имели бы, по (3),

0 = -0 = -(1 div 2) = (-1) div 2

Что, согласно (1), подразумевает

-1 = ((-1) div 2) * 2 + ((-1) mod 2) = 0 * 2 + ((-1) mod 2) = (-1) mod 2

Создание (-1) mod 2 < 0, которое противоречит (2).

Следовательно, нам нужно отказаться от некоторого свойства среди (1), (2) и (3).

Некоторые языки программирования отказываются от (3) и делают div округление вниз (Python, Ruby).

В некоторых (редких) случаях язык предлагает несколько операторов деления. Например, в Haskell мы имеем div,mod, удовлетворяющие только (1) и (2), подобно Python, и мы также имеем quot,rem, удовлетворяющие только (1) и (3). Последняя пара операторов округляет деление до нуля , по цене возвращая отрицательные остатки, например, мы имеем (-1) `quot` 2 = 0 и (-1) `rem` 2 = (-1).

C# также отказывается от (2) и позволяет % вернуть отрицательный остаток. Когерентно, целочисленное деление округляется до нуля. Java, Scala, Pascal и C, начиная с C99, также принимают эту стратегию.

Потому что смысл целочисленного деления состоит в том, что полный ответ включает остаток.

В Википедии есть большая статья об этом , включая историю, а также теорию.


До тех пор, пока язык удовлетворяет свойству евклидова деления, что (a/b) * b + (a%b) == a, и напольное деление, и усекающее деление когерентны и арифметически разумны.


Конечно, люди любят спорить о том, что одно очевидно правильно, а другое очевидно неправильно, но это больше похоже на священную войну, чем на разумную дискуссию, и обычно это имеет больше отношения к делу. с выбором их раннего предпочтительного языка, чем что-либо другое. Они также часто склонны спорить в первую очередь за свою избранную %, хотя, вероятно, имеет больше смысла сначала выбрать /, а затем просто выбрать %, который соответствует.
  • настил (как питон):
      Не меньший авторитет, чем Дональд Кнут, предлагает это.
  • % следование знаку делителя-это, по-видимому, то, о чем догадываются около 70% всех студентов
  • оператор обычно читается как mod или modulo вместо remainder.
  • "C делает это"-что даже не верно.1
  • усечение (как C++):
    • делает целочисленное деление более согласованным с IEEE float division (в режиме округления по умолчанию).
    • его реализуют другие процессоры. (Возможно, это не так в разные периоды истории.)
    • оператор читается modulo, а не remainder (хотя это фактически доказываетпротив их точки зрения).
    • свойство разделения концептуально это больше остаток, чем модуль.
    • оператор читается mod, а не modulo, поэтому он должен следовать различию Фортрана. (Это может показаться глупым, но, возможно, было решающим фактором для C99. Смотрите эту нить.)
  • "Евклидово" (подобно Паскалю-/ этажи или усечения в зависимости от знаков, поэтому % никогда не бывает отрицательным):
      Никлаус Вирт утверждал, что никто никогда не удивляется позитиву mod.
  • Раймонд т. Бут позже утверждал: что вы не можете реализовать евклидово деление наивно ни с одним из других правил.
  • Ряд языков обеспечивает и то, и другое. Обычно-как Ада, Модула-2, немного шепелявит, Хаскелл, и Юля-они используют имена, связанные с mod Для в стиле Python оператор и rem для C++-стиле оператора. Но не всегда-Фортран, например, называет одни и те же вещи modulo и mod (Как упоминалось выше для C99).


    Мы не знаем, почему Python, Tcl, Perl и другие влиятельные скриптовые языки в основном выбирали напольное покрытие. Как отмечалось в вопросе, ответ Гвидо ван Россума объясняет только то, почему он должен был выбрать один из трех последовательных ответов, а не то, почему он выбрал тот, который он сделал.

    Однако я подозреваю, что влияние С было ключевым. Большинство скриптовых языков (по крайней мере, изначально) реализованы на языке C, и заимствование их операторного инвентаря из реализации C. C89-defined % явно нарушено и не подходит для "дружественного" языка, такого как Tcl или Питон. И C вызывает оператора "mod". Поэтому они идут с модулем, а не остатком.

    1. Несмотря на то, что вопрос Говорит-и многие люди используют его в качестве аргумента-C на самом деле не имеет сходного поведения с Python и друзьями. C99 требует усечения деления,а не настила. C89 разрешил либо, а также разрешил любую версию mod, так что нет никакой гарантии свойства division, и нет способа написать переносимый код, делающий знаковое целочисленное деление. Это просто ... сломанный.

    Как сказала Паула, это из-за остатка.

    Алгоритм основан наевклидовом делении .

    В Ruby вы можете написать это перестроение дивидендов с последовательностью:

    puts (10/3)*3 + 10%3
    #=> 10
    

    То же самое происходит и в реальной жизни. 10 яблок и 3 человека. Хорошо, вы можете разрезать одно яблоко на три, но выходя за пределы заданных целых чисел.

    С отрицательными числами также сохраняется последовательность:

    puts (-10/3)*3 + -10%3 #=> -10
    puts (10/(-3))*(-3) + 10%(-3) #=> 10
    puts (-10/(-3))*(-3) + -10%(-3) #=> -10
    

    Частное всегда округлено вниз (вниз вдоль отрицательной оси) и ниже следует напоминание:

    puts (-10/3) #=> -4
    puts -10%3 #=> 2
    
    puts (10/(-3)) #=> -4
    puts 10%(-3) # => -2
    
    puts (-10/(-3)) #=> 3
    puts -10%(-3) #=> -1