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

17

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

6000.f * a * a;

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

6000.f * (a*a);

Или даже

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

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

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

    
задан lindelof 29.10.2011 в 00:48
источник
  • +1 для фактического запроса. –  R.. 29.10.2011 в 01:00

4 ответа

5

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

    
ответ дан R.. 29.10.2011 в 02:03
  • +1 действительно хороший ответ. –  ninjalj 29.10.2011 в 13:03
3

Не типично, нет.

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

    
ответ дан Reed Copsey 29.10.2011 в 00:57
  • Плюс к современным процессорам вы точно не знаете, в каком порядке выполняются операции или даже если они будут выполняться серийно. –  MartyTPS 29.10.2011 в 01:23
  • @MartyTPS: нерелевантно. Процессор вычисляет тот же результат, как если бы он выполнял операции поочередно. –  Dietrich Epp 29.10.2011 в 02:03
  • Обратите внимание, Дитрих. Возникает вопрос, важно ли изменение порядка. В конце концов, это не обязательно под вашим контролем, поэтому ДА это IRRELEVANT –  MartyTPS 29.10.2011 в 09:27
  • @MartyTPS: Вы ошибаетесь. Заказ находится под вашим контролем. Компилятор / процессор не могут переупорядочить операции с плавающей запятой каким-либо образом, чтобы изменить результат, и для большинства целей это означает, что они просто не могут переупорядочить плавающие точки вообще. –  R.. 30.10.2011 в 06:07
  • @MartyTPS Человек, который рассказал вам об исполнении вне заказа в современных процессорах, означал, что независимые инструкции могут выполняться в порядке, отличном от потока команд. Процессоры не переупорядочивают зависимые инструкции. en.wikipedia.org/wiki/Out-of-order_execution –  Pascal Cuoq 31.10.2011 в 12:02
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 , просто напишите его, когда вы его пишете, не нужно его менять; это нормально, как есть.

    
ответ дан Salvatore Previti 29.10.2011 в 01:00
  • Компилятор не может заменить умножение на деление, если результат не будет таким же, и это редко. Это может произойти только для двух сторон. (Тем не менее, -ffast-math на GCC позволит такую ​​оптимизацию.) –  Dietrich Epp 29.10.2011 в 01:02
  • Я говорил о делении с константами, microsoft visual C делает это, если нет никакого риска в этом. Он заменяет x / = 2; с х * = 0,5f; и это безопасно со всей точки зрения. –  Salvatore Previti 29.10.2011 в 01:08
  • Как я уже сказал, это может произойти только с полномочиями двух. Поэтому x / 3 не может быть изменено на умножение (если х не является целым числом, конечно ...) –  Dietrich Epp 29.10.2011 в 01:15
  • Да, конечно, сила двух может быть закодирована в плавающей точке без риска потери точности, другой вид десятичной дроби может быть не такой, но я думаю, что это зависит от компилятора и как вы указали на параметры оптимизации. Microsoft Visual C имеет флаг, который спрашивает, хотите ли вы точные или быстрые операции с плавающей запятой. –  Salvatore Previti 29.10.2011 в 01:22
  • По той же причине, что и для преобразования деления-> умножения, компилятор не может преобразовать 6.3f * a * 2.0f * 3.1f в * (6.3f * 2.0f * 3.1f), потому что это не то же самое , Компилятор, который делает это преобразование без программирования, запрашивающего его с помощью опции, не является оптимизирующим компилятором, это неправильный компилятор. Я согласен с тем, что программист может сам сделать преобразование, и для него это нормально, но причина, по которой компилятор этого не делает, - это отсутствие оптимизации. –  Pascal Cuoq 31.10.2011 в 12:06
2

Есть действительно алгоритмы для минимизации кумулятивной ошибки в последовательности операций с плавающей запятой. Одним из таких является Ссылка . Другие существуют для других операций: Ссылка .

    
ответ дан Thom Smith 15.06.2012 в 19:04