Почему языки по умолчанию не вызывают ошибок при целочисленном переполнении?

В нескольких современных языках программирования (включая C ++, Java и C #) язык позволяет целочисленное переполнение для происходят во время выполнения без возникновения каких-либо ошибок.

Например, рассмотрим этот (надуманный) метод C #, который не учитывает возможность переполнения / потери значимости. (Для краткости этот метод также не обрабатывает случай, когда указанный список является пустой ссылкой.)

//Returns the sum of the values in the specified list.
private static int sumList(List<int> list)
{
    int sum = 0;
    foreach (int listItem in list)
    {
        sum += listItem;
    }
    return sum;
}

Если этот метод вызывается следующим образом:

List<int> list = new List<int>();
list.Add(2000000000);
list.Add(2000000000);
int sum = sumList(list);

В методе sumList() произойдет переполнение (поскольку тип int в C # является 32-разрядным целым числом со знаком, а сумма значений в списке превышает значение максимального 32-разрядного целого числа со знаком). Переменная суммы будет иметь значение -294967296 (а не значение 4000000000); скорее всего, это не то, что предполагал (гипотетический) разработчик метода sumList.

Очевидно, что существуют различные методы, которые могут использоваться разработчиками, чтобы избежать возможности целочисленного переполнения, например, использование такого типа, как Java _ 5_ или _ 6_ ключевое слово и переключатель компилятора /checked в C #.

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

Сводится ли ответ просто к производительности - разработчики языков не хотели, чтобы в их соответствующих языках по умолчанию использовались «медленные» арифметические целочисленные операции, когда среде выполнения нужно было бы проделать дополнительную работу, чтобы проверить, произошло ли переполнение, для каждой применимой арифметики операция - и это соображение производительности перевешивает ценность предотвращения «тихих» сбоев в случае непреднамеренного переполнения?

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


person Jon Schneider    schedule 19.09.2008    source источник


Ответы (9)


В C # это был вопрос производительности. В частности, эталонный тест «из коробки».

Когда C # был новым, Microsoft надеялась, что многие разработчики C ++ перейдут на него. Они знали, что многие люди, работающие с C ++, думают о C ++ как о быстром, особенно быстром, чем языки, которые «тратят» время на автоматическое управление памятью и тому подобное.

Как потенциальные последователи, так и обозреватели журналов, скорее всего, получат копию нового C #, установят его, создадут тривиальное приложение, которое никто никогда не напишет в реальном мире, запустят его в жестком цикле и измерят, сколько времени это заняло. Затем они принимали решение за свою компанию или публиковали статью на основе этого результата.

Тот факт, что их тест показал, что C # работает медленнее, чем C ++, скомпилированный в собственном коде, - это то, что быстро отключит людей от C #. Тот факт, что ваше приложение C # будет автоматически обнаруживать переполнение / недостаточное заполнение, - это то, что они могут пропустить. Так что по умолчанию он выключен.

Я думаю, очевидно, что 99% времени мы хотим / проверять, чтобы они были включены. Это неудачный компромисс.

person Jay Bazuzi    schedule 20.09.2008
comment
Насколько проверка целочисленного переполнения повлияла бы на реалистичные тесты? Мне кажется, что в C # есть много других вещей, которые намного хуже (например, с учетом поля readonly rect foo;, такого оператора, как _2 _); `требует создания копии foo, вызова его метода доступа X, создания еще одной копии foo и вызова его Y метод доступа. Довольно огромные накладные расходы - достаточно, чтобы по сравнению с этим проверка целочисленного переполнения казалась довольно тривиальной. - person supercat; 23.12.2012
comment
@supercat: В большинстве случаев реального кода включение /checked по умолчанию не будет заметно медленнее и выявит определенный класс важных ошибок. Однако, как я сказал в своем ответе, первое, что может сделать ранний обозреватель C #, - это проверить производительность целочисленной арифметики в жестком цикле. - person Jay Bazuzi; 24.12.2012
comment
Помните, что C # исполнилось 12 лет. В то время многие программисты считали, что C ++ быстрый, Java медленный, C # похож на Java. Сегодня, я думаю, большинство программистов считают, что время выхода на рынок и управление сложностью имеют большее влияние на успех в бизнесе, чем жесткая оптимизация. - person Jay Bazuzi; 24.12.2012
comment
Была ли причина в производительности? Лично я часто полагаюсь на поведение (т.е. не отмечено по умолчанию), например счетчики последовательностей, которые я ожидаю зациклить, или в базе данных нет типа для значения без знака, но оно без знака, поэтому приведение просто работает ... - person markmnl; 17.12.2014

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

person David Hill    schedule 19.09.2008
comment
Разве ЦП не возвращает вам флаг при переполнении операции? (Конечно, проверка этого флага также требует времени). - person Thilo; 24.03.2017
comment
@Thilo есть процессоры без флагов вроде MIPS. Даже выполнение простых операций с большими int над ними - небольшая боль. - person phuclv; 14.08.2017

Вы работаете в предположении, что целочисленное переполнение всегда является нежелательным поведением.

Иногда целочисленное переполнение является желательным поведением. Один из примеров, который я видел, - это представление абсолютного значения заголовка в виде числа с фиксированной запятой. Для целого числа без знака 0 - это 0 или 360 градусов, а максимальное 32-битное целое число без знака (0xffffffff) является самым большим значением чуть ниже 360 градусов.

int main()
{
    uint32_t shipsHeadingInDegrees= 0;

    // Rotate by a bunch of degrees
    shipsHeadingInDegrees += 0x80000000; // 180 degrees
    shipsHeadingInDegrees += 0x80000000; // another 180 degrees, overflows 
    shipsHeadingInDegrees += 0x80000000; // another 180 degrees

    // Ships heading now will be 180 degrees
    cout << "Ships Heading Is" << (double(shipsHeadingInDegrees) / double(0xffffffff)) * 360.0 << std::endl;

}

Вероятно, существуют и другие ситуации, в которых допустимо переполнение, как в этом примере.

person Doug T.    schedule 19.09.2008
comment
Вероятно, существует гораздо больше случаев, когда целочисленное переполнение дает неправильные результаты, чем когда оно дает правильный результат. Фактически, единственный раз, когда он действительно генерирует правильные результаты, - это когда это ожидаемое поведение, и оно фактически используется как функция, например, в вашем примере. - person Kibbee; 19.09.2008
comment
@Kibbee Я бы рискнул предположить, что в случае, когда целочисленное переполнение может привести к ошибке, это скорее показатель того, что вы не выполнили надлежащую проверку диапазона, чем отказ языка программирования. В тех случаях, когда это не ожидается, ваш код должен проверить это. - person OwenP; 20.09.2008
comment
Не говоря уже о неотъемлемой опасности полагаться на такие низкоуровневые детали, специфичные для платформы. Что произойдет, если его перекомпилировать на 64-битной платформе? Eek. - person Dan; 17.12.2008
comment
@Dan: обратите внимание, что в коде используется uint32_t. Гарантированно 32-битный (отсюда и название :-)). Итак, автор действительно подумал об этом. - person sleske; 23.02.2010
comment
@sleske: Если вы проверите правки, вы увидите, что исходный пост Дуга использовал unsigned int ... отсюда и мой комментарий. Возможно, мой комментарий заставил его исправить свой код ... - person Dan; 26.02.2010
comment
Хотя есть места, где полезно иметь тип, который ведет себя как дискретное алгебраическое поле с определенным поведением обертывания, я бы сказал, что было бы более полезно иметь тип Int32, который проверяется, и тип WrappingInt32, который не является (аналогично для других размеров). Тогда код может легко получить нужное в любых обстоятельствах. - person supercat; 23.12.2012
comment
Я обнаружил, что чаще хочу упаковывать, чем нет, например счетчики последовательностей, которые я ожидаю зацикливать (поэтому могу просто увеличивать их), база данных не имеет беззнакового типа, поэтому приведение просто работает, например можно сохранить: (long) ulong.MaxValue и получить его снова с помощью (ulong) row [I]. - person markmnl; 17.12.2014
comment
Другой пример целочисленного переполнения в качестве желаемого поведения - вычитание, то есть сложение положительного числа с отрицательным числом того же абсолютного значения (так, чтобы их сумма была равна 0). - person wintermute; 16.01.2018

C / C ++ никогда не требует перехвата. Даже очевидное деление на 0 - это неопределенное поведение в C ++, а не особый вид ловушки.

В языке C нет концепции захвата, если вы не считаете сигналы.

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

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

Даже если бы C ++ сделал проверку целых чисел, 99% программистов в первые дни отключились бы, если бы отключили их для повышения производительности ...

person Steve Jessop    schedule 19.09.2008

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

person Dima    schedule 19.09.2008

Это скорее всего 99% работоспособность. На x86 придется проверять флаг переполнения при каждой операции, что сильно снизит производительность.

Другой 1% будет охватывать те случаи, когда люди выполняют причудливые манипуляции с битами или «неточно» смешивают операции со знаком и без знака и хотят семантику переполнения.

person Rob Walker    schedule 19.09.2008

Обратная совместимость очень важна. В C предполагалось, что вы уделяете достаточно внимания размеру ваших типов данных, чтобы в случае переполнения / потери значимости это было именно то, что вам нужно. Затем с C ++, C # и Java очень мало изменилось то, как работают «встроенные» типы данных.

person Eclipse    schedule 19.09.2008

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

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

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

person supercat    schedule 17.03.2020

Мое понимание того, почему ошибки не будут возникать по умолчанию во время выполнения, сводится к унаследованному желанию создавать языки программирования с поведением, подобным ACID. В частности, принцип, что все, что вы кодируете для выполнения (или не кодирования), оно будет делать (или не делать). Если вы не запрограммировали какой-либо обработчик ошибок, то машина из-за отсутствия обработчика ошибок «предположит», что вы действительно хотите сделать нелепую, подверженную сбоям вещь, которую вы ей говорите.

(Ссылка ACID: http://en.wikipedia.org/wiki/ACID)

person devinmoore    schedule 19.09.2008
comment
Я не вижу связи между свойствами ACID и тем, что вы описываете. Объясните, пожалуйста, связь ... - person Stephen C; 21.09.2009