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

StackOverflow https://stackoverflow.com/questions/103654

Вопрос

В нескольких современных языках программирования (включая 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-разрядного целого числа со знаком).Переменная sum будет иметь значение -294967296 (не значение 4000000000).;скорее всего, это не то, что предполагал (гипотетический) разработчик метода sumList.

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

Другие советы

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

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

Иногда желательным поведением является целочисленное переполнение.Один из примеров, который я видел, - это представление абсолютного значения заголовка в виде числа с фиксированной точкой.Учитывая unsigned int, 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;

}

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

C / C ++ никогда не предписывает поведение trap.Даже очевидное деление на 0 является неопределенным поведением в C ++, а не определенным видом ловушки.

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

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

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

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

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

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

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

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

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

(Ссылка на кислоту: http://en.wikipedia.org/wiki/ACID)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top