Почему языки не вызывают ошибки при целочисленном переполнении по умолчанию? - PullRequest
43 голосов
/ 19 сентября 2008

В некоторых современных языках программирования (включая 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 # фактически делает есть это .)

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

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

Ответы [ 8 ]

37 голосов
/ 20 сентября 2008

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

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

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

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

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

26 голосов
/ 19 сентября 2008

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

15 голосов
/ 19 сентября 2008

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

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

}

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

8 голосов
/ 19 сентября 2008

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

Язык Си не имеет никакого понятия о захвате, если только вы не подсчитываете сигналы.

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

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

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

7 голосов
/ 19 сентября 2008

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

6 голосов
/ 19 сентября 2008

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

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

4 голосов
/ 19 сентября 2008

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

0 голосов
/ 19 сентября 2008

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

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

...