Подтвердить что ты не робот

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

Ответ 3

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

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

}

Возможно, существуют другие ситуации, когда переполнение допустимо, аналогично этому примеру.

Ответ 4

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

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

Ответ 5

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

Ответ 6

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

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

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

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

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

Ответ 7

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

Ответ 8

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

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