Расчет с переменной вне ее границ в C - PullRequest
3 голосов
/ 26 марта 2010

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

Это пример того, что я спрашиваю:

int a, b;
a=30000;
b=(a*32000)/32767;

Я скомпилировал это, и он дает правильный ответ 29297 (ну, в любом случае, с усеченной ошибкой). Но меня беспокоит то, что 30 000 * 32 000 = 960 000 000, то есть 30-битное число, и, следовательно, не может быть сохранено в 16-битном целом. Конечный результат находится в пределах int, но я ожидал, что любая рабочая часть памяти будет иметь такой же размер, как и самые большие исходные переменные, поэтому возникнет ошибка переполнения.

Это лишь небольшой пример, демонстрирующий мою проблему. Я стараюсь избегать использования чисел с плавающей запятой, делая дробь дробной частью максимального количества, которое можно сохранить в этой переменной (в данном случае целое число со знаком, 32767 с положительной стороны), потому что я использую встроенную систему, я считаю, не имеет FPU.

Так как большинство процессоров обрабатывают вычисления вне границ исходной и целевой переменных?

Ответы [ 6 ]

3 голосов
/ 26 марта 2010

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

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

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

2 голосов
/ 26 марта 2010

Насколько я знаю, большинство, если не все процессоры, будут хранить результаты умножения слова * в двойное слово - это означает, что 8-битный * 8-бит сохраняется в 16-битном регистре (-ах)8-битный процессор, 32-битная * 32-битная операция сохраняется в 64-битном регистре (ах) на 32-битной машине.(По крайней мере, так было на всех встроенных микроконтроллерах, которые я использовал)

Если бы это было не так, процессор был бы сильно поврежден в том смысле, что допускал только половину слова * половину словаумножение слов.

0 голосов
/ 26 марта 2010

Ваш компилятор и целевой процессор связаны с размерами различных типов данных. Компиляторы обычно повышают переменные до максимально простого для работы с размером во время вычислений, а затем преобразуют результаты в любой размер, необходимый для назначения в конце. Существуют также правила C, регулирующие переход к размерам, с которыми труднее работать для некоторых вычислений. Если вы компилируете для AVR, который имеет 8-битные регистры, но определяет int как 16-битный, многие вычисления заканчивают тем, что используют больше регистров, чем вы думаете, из-за этого повышения и того факта, что константы в вашем коде следует рассматривать как int или unsigned int, если только компилятор не докажет себе, что это не повлияет на результат вычислений. Попробуйте переписать свой код с различными целыми числами различных размеров (short, int, long, long long) и посмотрите, как это получается. Вы также можете написать простую программу, которая печатает sizeof () стандартных предопределенных типов. Если вам нужно беспокоиться о размерах целочисленных переменных и / или промежуточных результатах ваших вычислений, вы должны включить и использовать такие вещи, как uint32_t и int64_t, для своих объявлений и приведения типов.

0 голосов
/ 26 марта 2010

Вы уверены, что ваш компилятор имеет 16-битные целые числа? В настоящее время большинство систем 32-битные. Другая возможная причина, по которой вы не получаете ошибку, заключается в том, что некоторые компиляторы распознают, что они могут вычислить что-то подобное во время компиляции, и делают это.

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

int a, b;
a=30000;
//b=(a*32000)/32767 ~= a * (32000/32768) = a *(125/128)
b = (a / 128) * 125 // if a=30000, b = 29250 - about 0.16% error

Другим вариантом будет использование типов большего размера для промежуточных терминов. Если ваш компилятор имеет 16-битные целые и 32-битные значения, вы можете сделать что-то вроде этого:

int a, b;
a=30000;
b=((long)a*32000L)/32767L;

Действительно, нет никакого определенного ответа на вопрос, как справиться с переполнением. Вы должны оценить каждый случай самостоятельно и решить, какое решение лучше.

0 голосов
/ 26 марта 2010

Целочисленное переполнение со знаком - неопределенное поведение.

Практически любая реализация, которую вы, возможно, встретите, будет охватывать целочисленное переполнение, потому что (a) каждый использует дополнение 2, в котором арифметические операции поразрядно идентичны для типов со знаком и без знака одинакового размера, и (b) обход определенное поведение типов без знака в C.

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

Обратите внимание, что если вы выполняете вычисления с двумя 16-битными short переменными на машине с 32-битным int, то вы обычно получите «правильный» ответ 29297, поскольку промежуточное значение (a*32000) является int, и в конце возвращается только к short. Я говорю «в общем», потому что преобразование целочисленного значения за пределами границ в целочисленный тип со знаком либо дает неопределенный результат, либо вызывает сигнал. Но опять же, любая реализация, с которой вы столкнетесь в вежливой компании, просто требует модуля.

0 голосов
/ 26 марта 2010

AFAIK, такие вещи формально не определены.Вы должны сделать алгебру, необходимую для предотвращения переполнения.Это всегда ваш первый выбор.Числовая стабильность не случайна, она требует определенной осторожности при принятии решения, когда и как выполнять деление и умножение.

Или вы должны гарантировать, что вы будете использовать достаточно большой промежуточный буфер результатов.

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

Итак, чтобы быть уверенным, что он работает, большинство людей делают что-то подобное.

short a= 30000;
int temp= a;
int temp2= (a*32000)/32767;
// here you can check for errors; if temp2 > 32767, you have overflow.
short b= a;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...