Есть ли предпочтительный способ упорядочения операндов с плавающей запятой?
Предположим, что у меня очень маленький float a
(например, a=0.5
), который входит в следующее выражение:
6000.f * a * a;
Оправдывает ли порядок операндов? Лучше ли писать
6000.f * (a*a);
Или даже
float result = a*a;
result *= 6000.f;
Я проверил классический Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой, но ничего не мог найти.
Существует ли оптимальный способ упорядочения операндов в операции с плавающей запятой?
Ответы
Ответ 1
Это действительно зависит от ценностей и ваших целей. Например, если a
очень мало, a*a
может быть нулевым, тогда как 6000.0*a*a
(что означает (6000.0*a)*a
) все равно может быть отличным от нуля. Для избежания переполнения и недоиспользования общим правилом является применение ассоциативного закона для первого выполнения умножений, когда журналы операндов имеют противоположный знак, что означает, что сначала квадрат является худшей стратегией. С другой стороны, по соображениям производительности квадрат сначала может быть очень хорошей стратегией, если вы можете повторно использовать значение квадрата. Вы можете столкнуться с еще одной проблемой, которая может иметь большее значение для правильности, чем проблемы с переполнением/недостаточным потоком, если ваши номера никогда не будут очень близки к нулю или бесконечности: некоторые умножения могут быть гарантированы точными ответами, а другие - округлением. В общем, вы получите самые точные результаты, минимизируя количество шагов округления, которые происходят.
Ответ 2
Не типично, нет.
При этом, если вы выполняете несколько операций с большими значениями, имеет смысл упорядочить их таким образом, чтобы избежать переполнения или уменьшить точность ошибок на основе их приоритет и ассоциативность, если алгоритм обеспечивает способ сделать это очевидным. Это, однако, требует предварительного знания соответствующих значений, а не просто основано на синтаксисе.
Ответ 3
Оптимальный способ зависит от цели, действительно.
Прежде всего, умножение происходит быстрее, чем деление.
Итак, если вам нужно написать a = a / 2;
, лучше написать a = a * 0.5f;
.
Ваш компилятор обычно достаточно умный, чтобы заменить деление на умножение на константы, если результаты одинаковы, но он не будет делать этого с переменными, конечно.
Иногда вы можете немного оптимизировать, заменив деления на умножения, но могут быть проблемы с точностью.
Некоторые другие операции могут быть более быстрыми, но менее точными.
Возьмем пример.
float f = (a * 100000) / (b * 10);
float g = (a / b) * (100000 / 10);
Они математически эквивалентны, но результат может быть немного иным.
Первый использует два умножения и одно деление, второе использует одно деление и одно умножение. В обоих случаях может быть потеря точности, она зависит от размера a и b, если они являются небольшими значениями, сначала лучше работает, если они являются большими значениями, второй лучше работает
Затем... если у вас есть несколько констант, и вы хотите скорость, группируйте группы вместе.
float a = 6.3f * a * 2.0f * 3.1f;
Просто напишите
a = a * (6.3f * 2.0f * 3.1f);
Некоторые компиляторы хорошо оптимизируют, другие оптимизируют меньше, но в обоих случаях нет риска держать все константы вместе.
После того, как мы это скажем, мы должны часами говорить о том, как работают процессоры.
Даже одна и та же семья, такая как intel, работает по-другому между поколениями!
Некоторые компиляторы используют инструкции SSE, другие - нет.
Некоторые процессоры поддерживают SSE2, некоторые SSE, некоторые только MMX... в какой-то системе нет FPU!
Каждая система лучше выполняет некоторые вычисления, чем другие, найти общую вещь сложно.
Вы должны просто написать читаемый код, чистый и простой, не слишком беспокоясь об этих непредсказуемых очень низких уровнях оптимизации.
Если ваше выражение выглядит сложным, сделайте некоторую алгебру и\или перейдите в поисковую систему wolframalpha и попросите его оптимизировать это для вас:)
Сказано, что вам действительно не нужно объявлять одну переменную и снова и снова заменять ее содержимое, компилятор обычно может оптимизировать меньше в этой ситуации.
a = 5 + b;
a /= 2 * c;
a += 2 - c;
a *= 7;
просто напишите свое выражение, избегая этого беспорядка:)
a = ((5 + b) / (2 * c) + 2 - c) * 7;
В вашем конкретном примере 6000.f * a * a
просто напишите его, как вы его пишете, не нужно его менять; это нормально, как есть.
Ответ 4
Есть действительно алгоритмы для минимизации кумулятивной ошибки в последовательности операций с плавающей запятой. Одним из таких является http://en.wikipedia.org/wiki/Kahan_summation_algorithm. Другие существуют для других операций: http://www.cs.cmu.edu/~quake-papers/related/Priest.ps.