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

Как я могу сохранить порядок умножений и делений?

В моем 32-битном встроенном приложении С++ мне нужно выполнить следующее вычисление:

calc(int milliVolts, int ticks) {
  return milliVolts * 32767 * 65536 / 1000 / ticks;
}

Теперь, поскольку int на моей платформе имеет 32 бита, а milliVolts имеет диапазон [-1000: 1000], часть milliVolts * 32767 * 65536 может вызвать целочисленное переполнение. Чтобы этого избежать, я разделил коэффициент 65536 на 1024, 32 и переупорядочил следующим образом:

calc(int milliVolts, int ticks) {
  return milliVolts*32767*32/1000*1024/ticks*2;
}

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

Состояние Кернингхана и Ричи в разделе 2.12 "Язык программирования C" (у меня нет стандартной версии С++):

C, как и большинство языков, не указывается порядок, в котором оцениваются операнды оператора.

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

Как я могу написать свою функцию таким образом, чтобы она работала?

РЕДАКТИРОВАТЬ: Несколько ответов ниже предлагают использовать вычисления с плавающей запятой, чтобы избежать этой проблемы. Это не вариант, потому что код работает на процессоре, который не имеет операций с плавающей запятой. Кроме того, вычисление выполняется в жесткой части приложения в реальном времени, поэтому ограничение скорости использования эмулируемой плавающей запятой слишком велико.

ЗАКЛЮЧЕНИЕ: С помощью ответа Мердада и комментария Мэтта Макнабба мне удалось найти соответствующий раздел в разделе K & R, раздел A7, где говорится:

Приоритет и ассоциативность операторов полностью определены, но порядок оценки выражений с определенными исключениями undefined, даже если подвыражения связаны с побочными эффектами. То есть, если определение оператора не гарантирует, что его операнды оцениваются в определенном порядке, реализация может свободно оценивать операнды в любом порядке или даже чередовать их оценку. Однако каждый оператор объединяет значения, создаваемые его операндами, таким образом, который совместим с разбором выражений, в которых он появляется. Это правило отменяет предыдущую свободу для переупорядочения выражений с операторами, которые являются математически коммутативными и ассоциативными, но не могут быть вычислительно ассоциативными. Это изменение влияет только на вычисления с плавающей запятой вблизи пределов их точности и ситуаций, когда возможно переполнение.

Итак, Мердад прав: беспокоиться не о чем.

4b9b3361

Ответ 1

На самом деле:

Вы (и другие) не понимаете, что говорится. Здесь нет проблем.

Они говорят, что если у вас есть f(x) + g(y), нет гарантии, что f(x) оценивается до g(y) (или наоборот).

Но если у вас

milliVolts * 32767 * 32 / 1000 * 1024 / ticks * 2

он автоматически интерпретируется как

(((((milliVolts * 32767) * 32) / 1000) * 1024) / ticks) * 2

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

Таким образом, не о чем беспокоиться.

Ответ 2

(Это скорее комментарий, но слишком длинный, чтобы вписаться в поле комментариев)


"Я буду использовать круглые скобки. Даже для того, чтобы сделать код более понятным.

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

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

Ответ 3

Моим ответом было бы бросить милливольты и тики на int64_t и выполнить вычисления. Затем утвердите, если результаты могут быть сохранены в int.

EDIT: datahaki предлагают использовать значения с плавающей запятой в вычислении. Это может оказаться полезным, потому что вы, скорее всего, закончите вычисление с помощью фракций.

Ответ 4

У вас всегда будет определенная ошибка округления, я думаю, что это не нуждается в объяснении. Таким образом, стратегия состоит в том, чтобы попытаться максимально минимизировать ошибку, учитывая, что мы не знаем а-priory значения милливольта и тиков.

Я предлагаю разделить расчет в два этапа. Во-первых, группируя ваши константы, мы имеем: 32767 * 65536/1000 = 2147418.112 = 2147418 с погрешностью 0,122, что составляет всего лишь 1 часть в 20 миллионов, примерно.

Итак, объявим const int:

const int factor = 2147418;

Теперь milivolts находится в диапазоне [-1000,1000] и галочки в диапазоне [1,1024]. Если мы вычислим милливольты/тики, мы можем иметь большие ошибки; рассмотрим, например, ситуацию:

millivolts=1;
ticks=1024;
intermediate = millivolts/ticks = 0;
result = intermediate * factor = 0; /// Big error, as result should be 2097

Поэтому я предлагаю следующее:

int intermediate = factor * millivolt;
int result = intermediate / ticks;

Таким образом, в худшем случае (милливольт = 1000) промежуточное значение подходит для 32-разрядного целого числа, которое, не имея никакой информации об обратном, предположим, что вы можете использовать.