Есть ли предпочтительный способ заказа операндов с плавающей точкой? - PullRequest
18 голосов
/ 29 октября 2011

Предположим, у меня есть очень маленький float a (например, a=0.5), который вводит следующее выражение:

6000.f * a * a;

Имеет ли значение порядок операндов? Лучше написать

6000.f * (a*a);

Или даже

float result = a*a;
result *= 6000.f;

Я проверил классическую Что должен знать каждый учёный об арифметике с плавающей точкой , но ничего не смог найти.

Есть ли оптимальный способ упорядочить операнды в операции с плавающей запятой?

Ответы [ 4 ]

6 голосов
/ 29 октября 2011

Это действительно зависит от ценностей и ваших целей.Например, если a очень мало, a*a может быть нулем, тогда как 6000.0*a*a (что означает (6000.0*a)*a) все еще может быть ненулевым.Чтобы избежать переполнения и недопущения, общее правило состоит в том, чтобы применять ассоциативный закон, чтобы сначала выполнить умножения, где у журналов операндов есть противоположный знак, что означает, что возведение в квадрат сначала - вообще худшая стратегия.С другой стороны, по соображениям производительности возведение в квадрат сначала может быть очень хорошей стратегией, если вы можете повторно использовать значение квадрата.Вы можете столкнуться с еще одной проблемой, которая может иметь большее значение для правильности, чем проблемы переполнения / недополнения, если ваши числа никогда не будут очень близки к нулю или бесконечности: некоторые умножения могут гарантировать точные ответы, в то время как другие включают округление.В целом вы получите наиболее точные результаты, сводя к минимуму количество шагов округления.

3 голосов
/ 29 октября 2011

Оптимальный способ зависит от цели, действительно.

Прежде всего, умножение быстрее деления.

Так что, если вам нужно написать 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, просто напишите его, как вы пишете, не нужно его менять; это нормально, как есть.

3 голосов
/ 29 октября 2011

Обычно нет, нет.

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

2 голосов
/ 15 июня 2012

Действительно, существуют алгоритмы, позволяющие минимизировать накопленную ошибку в последовательности операций с плавающей запятой. Один такой http://en.wikipedia.org/wiki/Kahan_summation_algorithm. Другие существуют для других операций: http://www.cs.cmu.edu/~quake-papers/related/Priest.ps.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...