Почему GCC не оптимизирует a * a * a * a * a * a до (a * a * a) * (a * a * a)?

2120

Я делаю некоторую числовую оптимизацию для научного приложения. Одна вещь, которую я заметил, заключается в том, что GCC оптимизирует вызов pow(a,2), компилируя его a*a, но вызов pow(a,6)не оптимизируется и фактически вызовет библиотечную функцию pow, что значительно снижает производительность. (В отличие от этого , исполняемый файл компилятора Intel C ++icc исключает необходимость использования библиотеки pow(a,6).)

Что меня интересует, так это то, что при замене pow(a,6)с a*a*a*a*a*aиспользованием GCC 4.5.1 и параметров " -O3 -lm -funroll-loops -msse4" используются 5 mulsdинструкций:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

а если я напишу (a*a*a)*(a*a*a), то выдаст

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

который уменьшает количество умножаемых инструкций до 3. iccимеет аналогичное поведение.

Почему компиляторы не распознают этот прием оптимизации?

Xis
источник
13
Что значит «распознать pow (a, 6)»?
Варун Мадиаф
659
Гм ... вы знаете, что a a a a a a и (a a a) * (a a * a) не совпадают с числами с плавающей запятой, не так ли? Вам придется использовать -funsafe-math или -ffast-math или что-то для этого.
Дэймон
106
Я предлагаю вам прочитать «Что должен знать каждый компьютерный ученый об арифметике с плавающей точкой» Дэвида Голдберга: download.oracle.com/docs/cd/E19957-01/806-3568/… после чего у вас будет более полное понимание смоляная яма, в которую вы только что вошли!
Фил Армстронг
189
Совершенно разумный вопрос. 20 лет назад я задал тот же общий вопрос и, сократив это единственное узкое место, сократил время выполнения симуляции Монте-Карло с 21 часа до 7 часов. Код во внутреннем цикле был выполнен 13 триллионов раз в процессе, но он перенес симуляцию в ночное окно. (см. ответ ниже)
23
Может быть, бросить (a*a)*(a*a)*(a*a)в смесь тоже. Такое же количество умножений, но, возможно, более точное.
Рок Краль

Ответы:

2739

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

В результате большинство компиляторов очень консервативно изменяют порядок вычислений с плавающей запятой, если только они не могут быть уверены, что ответ останется прежним, или если вы не скажете им, что вам не важна числовая точность. Например: вариант НКИ , который позволяет куб.см до реассоциируют операции с плавающей точкой, или даже вариант , который позволяет даже более агрессивные компромиссы точности в отношении скорости.-fassociative-math-ffast-math

Lambdageek
источник
10
Да. С -ffast-math это делает такую ​​оптимизацию. Хорошая идея! Но поскольку наш код касается большей точности, чем скорости, может быть, лучше его не пропустить.
XIs
19
IIRC C99 позволяет компилятору выполнять такие «небезопасные» оптимизации FP, но GCC (на любом другом устройстве, кроме x87) делает разумную попытку следовать IEEE 754 - это не «границы ошибок»; есть только один правильный ответ .
тк.
14
Детали реализации powни здесь, ни там; этот ответ даже не ссылка pow.
Стивен Кэнон
14
@nedR: ICC по умолчанию разрешает повторную ассоциацию. Если вы хотите получить поведение, соответствующее стандартам, вам нужно установить его -fp-model preciseс помощью ICC. clangи по gccумолчанию строгое соответствие с повторной ассоциацией.
Стивен Кэнон,
49
@ xis, это не совсем -fassociative-mathтак; это просто так a*a*a*a*a*aи (a*a*a)*(a*a*a)разные. Дело не в точности; это касается соответствия стандартам и строго повторяемых результатов, например, одинаковых результатов на любом компиляторе. Числа с плавающей точкой уже не точны. Редко неуместно компилировать -fassociative-math.
Пол Дрэйпер
652

Lambdageek правильно указывает, что поскольку ассоциативность не выполняется для чисел с плавающей запятой, «оптимизация»a*a*a*a*a*ato(a*a*a)*(a*a*a)может изменить значение. Вот почему он запрещен C99 (если это явно не разрешено пользователем, с помощью флага компилятора или прагмы). Как правило, предполагается, что программист написал то, что она сделала по какой-то причине, и компилятор должен это учитывать. Если хочешь(a*a*a)*(a*a*a), напиши это.

Это может быть боль писать, хотя; почему компилятор не может просто [сделать то, что вы считаете] правильным, когда вы используете pow(a,6)? Потому что это было бы неправильно . На платформе с библиотекой хорошей математики, pow(a,6)является значительно более точным , чем либо a*a*a*a*a*aили (a*a*a)*(a*a*a). Просто для того, чтобы предоставить некоторые данные, я провел небольшой эксперимент на своем Mac Pro, измеряя наихудшую ошибку при оценке ^ 6 для всех плавающих чисел одинарной точности между [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

Использование powвместо дерева умножения уменьшает погрешность в 4 раза . Компиляторы не должны (и, как правило, не делают) делать «оптимизации», которые увеличивают ошибку, если только у пользователя нет на это лицензии (например, через -ffast-math).

Обратите внимание, что GCC предоставляет __builtin_powi(x,n)в качестве альтернативы pow( ), которая должна генерировать встроенное дерево умножения. Используйте это, если вы хотите поменять точность на производительность, но не хотите включать быструю математику.

Стивен Кэнон
источник
29
Также обратите внимание, что Visual C ++ предоставляет «улучшенную» версию pow (). Позвонив _set_SSE2_enable(<flag>)с flag=1, что , если это возможно будет использовать SSE2. Это немного снижает точность, но повышает скорость (в некоторых случаях). MSDN: _set_SSE2_enable () и pow ()
TkTech
18
@TkTech: Любое снижение точности связано с реализацией Microsoft, а не с размером используемых регистров. Можно поставить правильно округленный, pow используя только 32-битные регистры, если автор библиотеки так мотивирован. Существуют powреализации на основе SSE, которые являются более точными, чем большинство реализаций на основе x87, а также есть реализации, которые обменивают некоторую точность на скорость.
Стивен Кэнон
9
@TkTech: Конечно, я просто хотел пояснить, что снижение точности связано с выбором, сделанным авторами библиотек, а не с использованием SSE.
Стивен Кэнон
7
Мне интересно знать, что вы использовали в качестве «золотого стандарта» здесь для расчета относительных ошибок - я обычно ожидал, что это произойдет a*a*a*a*a*a, но это, очевидно, не так! :)
j_random_hacker
8
@j_random_hacker: поскольку я сравнивал результаты с одинарной точностью, для золотого стандарта достаточно двойной точности - ошибка от a a a a a a, вычисляемая в двойном значении, * значительно меньше, чем ошибка любого из вычислений с одинарной точностью.
Стивен Кэнон
168

Другой подобный случай: большинство компиляторов не будет оптимизировать a + b + c + dдля (a + b) + (c + d)(это оптимизация , так как второе выражение может быть конвейерными лучше) и оценить его , как указано (например , как (((a + b) + c) + d)). Это тоже из-за угловых случаев:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Это выводы 1.000000e-05 0.000000e+00

sanjoyd
источник
10
Это не совсем то же самое. Изменение порядка умножения / деления (исключая деление на 0) безопаснее, чем изменение порядка сумм / вычитаний. По моему скромному мнению, компилятор должен попытаться связать mults ./divs. потому что это уменьшает общее количество операций и, кроме увеличения производительности, повышает точность.
CoffeDeveloper
4
@DarioOO: это не безопаснее. Умножение и деление такие же, как сложение и вычитание показателя степени, и изменение порядка может легко привести к тому, что временные значения превысят возможный диапазон показателя степени. (Не совсем то же самое, потому что показатель степени не страдает потерей точности ... но представление все еще весьма ограничено, и изменение порядка может привести к непредставимым значениям)
Бен Фойгт
8
Я думаю, что вам не хватает фона исчисления. Умножение и деление 2 чисел приводят к одинаковой величине ошибки. Хотя вычитание / сложение 2-х чисел может привести к большей ошибке, особенно когда 2-е числа на порядок отличаются друг от друга, следовательно, более безопасно переставить множитель / деление, чем саб / сложение, потому что оно вносит незначительное изменение в окончательную ошибку.
CoffeDeveloper
8
@DarioOO: риск отличается от mul / div: изменение порядка вносит незначительное изменение в конечный результат, или экспонента переполняется в некоторой точке (где это не было бы раньше), и результат существенно отличается (потенциально + inf или 0).
Питер Кордес
@GameDeveloper Внедрение точного выигрыша непредсказуемым образом чрезвычайно проблематично.
любопытный парень
80

Fortran (разработанный для научных вычислений) имеет встроенный оператор питания, и, насколько я знаю, компиляторы Fortran обычно оптимизируют повышение до целочисленных степеней аналогично тому, что вы описываете. К сожалению, в C / C ++ нет оператора power, только библиотечная функция pow(). Это не мешает умным компиляторам обрабатывать их powособым образом и ускорять вычисления для особых случаев, но, похоже, они делают это реже ...

Несколько лет назад я пытался сделать более удобным расчет целочисленных степеней оптимальным способом и придумал следующее. Это C ++, а не C, и все еще зависит от умения компилятора оптимизировать / встроить вещи. В любом случае, надеюсь, вы найдете это полезным на практике:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

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

Тогда просто используйте это как power<6>(a).

Это позволяет легко набирать полномочия (не нужно прописывать 6 aс паренами) и позволяет проводить оптимизацию такого рода, -ffast-mathесли у вас есть что-то зависящее от точности, например, скомпенсированное суммирование (пример, где порядок операций важен) ,

Вы также можете забыть, что это C ++, и просто использовать его в программе на C (если он компилируется с помощью компилятора C ++).

Надеюсь, что это может быть полезно.

РЕДАКТИРОВАТЬ:

Вот что я получаю от моего компилятора:

Для a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Для (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Для power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
Сабольч
источник
36
Найти оптимальное дерево мощностей может быть сложно, но, поскольку оно интересно только для малых мощностей, очевидный ответ - предварительно рассчитать его один раз (Кнут предоставляет таблицу до 100) и использовать эту жестко закодированную таблицу (это то, что gcc делает внутренне для powi) ,
Марк Глисс
7
На современных процессорах скорость ограничена задержкой. Например, результат умножения может быть доступен после пяти циклов. В этой ситуации найти самый быстрый способ создать какую-то силу может быть сложнее.
gnasher729
3
Вы также можете попытаться найти дерево мощности, которое дает самую низкую верхнюю границу для относительной ошибки округления или самую низкую среднюю относительную ошибку округления.
gnasher729
1
Boost также поддерживает это, например boost :: math :: pow <6> (n); Я думаю, что он даже пытается уменьшить количество умножений путем извлечения общих факторов.
gast128
Обратите внимание, что последний эквивалентен (a ** 2) ** 3
minmaxavg
62

GCC на самом деле оптимизации a*a*a*a*a*aдля , (a*a*a)*(a*a*a)когда целое. Я попытался с этой командой:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Есть много флагов gcc, но ничего особенного. Они означают: читать со стандартного ввода; использовать уровень оптимизации O2; выводит список ассемблера вместо двоичного файла; в листинге должен использоваться синтаксис языка ассемблера Intel; ввод осуществляется на языке C (обычно язык определяется по расширению входного файла, но при чтении из stdin расширение файла отсутствует); и написать в стандартный вывод.

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

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Я использую систему GCC на Linux Mint 16 Petra, производной от Ubuntu. Вот версия gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

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

picomancer
источник
12
Это допустимо для целочисленного умножения, потому что переполнение в двух дополнениях - неопределенное поведение. Если произойдет переполнение, это произойдет где-то независимо от операций переупорядочения. Таким образом, выражения без переполнения оценивают одинаково, выражения с переполнением - неопределенное поведение, поэтому компилятор может изменить точку, в которой происходит переполнение. GCC делает это unsigned intтоже.
Питер Кордес
51

Потому что 32-разрядное число с плавающей запятой, такое как 1.024, не равно 1.024. В компьютере 1,024 - это интервал: от (1,024-е) до (1,024 + е), где «е» представляет ошибку. Некоторые люди не понимают этого и также считают, что * в * a означает умножение чисел произвольной точности без каких-либо ошибок, связанных с этими числами. Причиной, по которой некоторые люди не понимают этого, возможно, являются математические вычисления, которые они выполняли в начальных школах: работать только с идеальными числами без ошибок и полагать, что можно просто игнорировать «е» при выполнении умножения. Они не видят «e», подразумеваемое в «float a = 1.2», «a * a * a» и аналогичных C-кодах.

Если большинство программистов признают (и смогут выполнять) идею о том, что выражение C a * a * a * a * a * a на самом деле не работает с идеальными числами, компилятор GCC тогда БЕСПЛАТНО оптимизирует "a * a * a * a * a * a "в" t "(a * a); t * t * t", что требует меньшего числа умножений. Но, к сожалению, компилятор GCC не знает, думает ли программист, пишущий код, что «a» - это число с ошибкой или без нее. И поэтому GCC будет делать только то, на что похож исходный код - потому что это то, что GCC видит невооруженным глазом.

... как только вы узнаете, какой вы программист , вы можете использовать переключатель "-ffast-math", чтобы сообщить GCC: "Эй, GCC, я знаю, что я делаю!" Это позволит GCC преобразовать a * a * a * a * a * a в другой фрагмент текста - он выглядит иначе, чем a * a * a * a * a * a - но все равно вычисляет число в интервале ошибок а * а * а * а * а * а. Это нормально, так как вы уже знаете, что работаете с интервалами, а не с идеальными числами.


источник
52
Числа с плавающей точкой являются точными. Они просто не совсем то, что вы ожидали. Более того, методика с эпсилоном сама по себе является приближением к тому, как решать вещи в реальности, потому что истинная ожидаемая ошибка зависит от масштаба мантиссы, то есть вы обычно достигаете примерно 1 LSB, но это может увеличиваться с каждая операция выполняется, если вы не осторожны, поэтому проконсультируйтесь с числовым аналитиком, прежде чем делать что-то нетривиальное с плавающей запятой. Используйте подходящую библиотеку, если возможно.
Донал Феллоуз
3
@DonalFellows: Стандарт IEEE требует, чтобы вычисления с плавающей запятой давали результат, который наиболее точно соответствует результату, если бы исходные операнды были точными значениями, но это не означает, что они фактически представляют точные значения. Во многих случаях более полезно рассматривать 0,1f как (1 677 722 +/- 0,5) / 16 777 216, который должен отображаться с количеством десятичных цифр, подразумеваемых этой неопределенностью, чем рассматривать его как точное количество (1 677 722 +/- 0,5) / 16,777,216 (который должен отображаться до 24 десятичных цифр).
суперкат
23
@supercat: IEEE-754 довольно ясно говорит о том, что данные с плавающей точкой действительно представляют точные значения; пункты 3.2 - 3.4 являются соответствующими разделами. Разумеется, вы можете интерпретировать их иначе, точно так же, как вы можете интерпретировать их int x = 3как значение x3 +/- 0,5.
Стивен Кэнон
7
@supercat: Я полностью согласен, но это не значит, что Distanceон не равен его числовому значению; это означает, что числовое значение является лишь приближением к некоторой физической моделируемой величине.
Стивен Кэнон
10
За численный анализ ваш мозг поблагодарит вас, если вы интерпретируете числа с плавающей запятой не как интервалы, а как точные значения (которые оказываются не совсем теми значениями, которые вы хотели). Например, если x где-то около 4,5 с ошибкой менее 0,1, и вы вычисляете (x + 1) - x, интерпретация «интервал» оставляет вас с интервалом от 0,8 до 1,2, тогда как интерпретация «точное значение» сообщает у вас результат будет 1 с ошибкой не более 2 ^ (- 50) с двойной точностью.
gnasher729
34

Никто из авторов еще не упомянул о сокращении выражений с плавающей запятой (стандарт ISO C, 6.5p8 и 7.12.2). Если для FP_CONTRACTпрагмы задано значение ON, компилятору разрешается рассматривать выражение, например, как a*a*a*a*a*aодну операцию, как если бы оно вычислялось точно с одним округлением. Например, компилятор может заменить его внутренней функцией power, которая быстрее и точнее. Это особенно интересно, поскольку поведение частично контролируется программистом непосредственно в исходном коде, в то время как параметры компилятора, предоставляемые конечным пользователем, иногда могут использоваться неправильно.

Состояние FP_CONTRACTпрагмы по умолчанию определяется реализацией, поэтому компилятору разрешено выполнять такую ​​оптимизацию по умолчанию. Таким образом, переносимый код, который должен строго следовать правилам IEEE 754, должен явно установить его OFF.

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

GCC не поддерживает эту прагму, но с параметрами по умолчанию, она предполагает, что это так ON; таким образом, для целей с аппаратным FMA, если кто-то хочет предотвратить преобразование a*b+cв fma (a, b, c), нужно предоставить опцию, такую ​​как -ffp-contract=off(явно установить прагму OFF) или -std=c99(чтобы сказать GCC, чтобы он соответствовал некоторым C стандартная версия, здесь C99, таким образом, следуйте вышеупомянутому параграфу). В прошлом последний вариант не препятствовал преобразованию, а это означает, что GCC не соответствовал этому пункту: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845.

vinc17
источник
3
Долгоживущие популярные вопросы иногда показывают их возраст. Этот вопрос был задан и получен ответ в 2011 году, когда GCC можно было извинить за то, что он не соблюдает в точности тот недавний стандарт C99. Конечно, сейчас 2014, так что GCC ... хм.
Паскаль Куок
Разве вы не должны отвечать на сравнительно недавние вопросы с плавающей запятой без принятого ответа? кашель stackoverflow.com/ru/questions/23703408 кашель
Паскаль Куок
Я нахожу это ... тревожным, что gcc не реализует прагмы с плавающей точкой C99.
Дэвид Моннио
1
Прагмы @DavidMonniaux по определению необязательны для реализации.
Тим Сегин
2
@TimSeguine Но если прагма не реализована, ее значение по умолчанию должно быть наиболее ограничивающим для реализации. Полагаю, об этом думал Давид. В GCC это теперь исправлено для FP_CONTRACT, если используется режим ISO C : он по-прежнему не реализует прагму, но в режиме ISO C он теперь предполагает, что прагма отключена.
Vinc17
28

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

Bjorn
источник
3
@greggo Нет, тогда все еще детерминировано. Никакой случайности не добавляется ни в каком смысле слова.
Алиса
9
@ Алиса Кажется, довольно ясно, что Бьорн здесь использует «детерминированный» в смысле кода, дающего одинаковый результат на разных платформах и разных версиях компилятора и т. Д. (Внешние переменные, которые могут быть вне контроля программиста) - в отличие от отсутствия фактической числовой случайности во время выполнения. Если вы указываете, что это неправильное использование слова, я не буду спорить с этим.
Грегго
5
@greggo За исключением даже в вашей интерпретации того, что он говорит, это все еще неправильно; в этом весь смысл IEEE 754 - обеспечить идентичные характеристики для большинства (если не для всех) операций на разных платформах. Теперь он не упомянул о платформах или версиях компилятора, что было бы очень кстати, если бы вы хотели, чтобы каждая операция на каждом удаленном сервере / клиенте была одинаковой ... но это не очевидно из его заявления. Лучшее слово может быть «надежно похожим» или что-то в этом роде.
Алиса
8
@ Алиса, что вы тратите время каждого, включая свое собственное, споря о семантике. Его смысл был ясен.
Ланару
11
@Lanaru Суть стандартов - это семантика; его смысл был явно неясен.
Алиса
28

Библиотечные функции, такие как «pow», обычно тщательно создаются для получения минимально возможной ошибки (в общем случае). Обычно это достигается аппроксимацией функций сплайнами (согласно комментарию Паскаля, наиболее распространенной реализацией, похоже, является использование алгоритма Ремеза )

Принципиально следующая операция:

pow(x,y);

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

Пока следующая операция:

float a=someValue;
float b=a*a*a*a*a*a;

имеет собственную ошибку, которая более чем в 5 раз превышает ошибку одиночного умножения или деления (потому что вы комбинируете 5 умножений).

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

  1. если оптимизация pow(a,6)к a*a*a*a*a*aнему может улучшить производительность, но резко снизить точность для чисел с плавающей запятой.
  2. если оптимизация a*a*a*a*a*a к pow(a,6)нему может фактически снизить точность, потому что «a» было некоторым специальным значением, которое позволяет умножение без ошибок (степень 2 или небольшое целое число)
  3. если оптимизация pow(a,6)для (a*a*a)*(a*a*a)или (a*a)*(a*a)*(a*a)там еще может быть потеря точности по сравнению с powфункцией.

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

Единственное, что имеет смысл (личное мнение и, очевидно, выбор в GCC без какой-либо конкретной оптимизации или флага компилятора) для оптимизации, - это заменить "pow (a, 2)" на "a * a". Это было бы единственной разумной вещью, которую должен сделать поставщик компилятора.

CoffeDeveloper
источник
7
downvoters должны понимать, что этот ответ совершенно нормально. Я могу процитировать десятки источников и документации в поддержку своего ответа, и я, вероятно, больше связан с точностью с плавающей запятой, чем любой даунотер. В StackOverflow совершенно разумно добавлять недостающую информацию, которая не покрывается другими ответами, поэтому будьте вежливы и объясните свои причины.
CoffeDeveloper
1
Мне кажется, что ответ Стивена Канона охватывает то, что вы должны сказать. Похоже, вы настаиваете, что libms реализованы с помощью сплайнов: они чаще используют сокращение аргументов (в зависимости от реализуемой функции) плюс один полином, коэффициенты которого были получены с помощью более или менее сложных вариантов алгоритма Ремеза. Плавность в точках соединения не считается объективной целью для функций libm (если они оказываются достаточно точными, они все равно автоматически становятся достаточно плавными независимо от того, на сколько частей был разбит домен).
Паскаль Куок
Во второй половине вашего ответа совершенно не хватает того, что компиляторы должны создавать код, который реализует то, что говорит исходный код, точка. Также вы используете слово «точность», когда вы имеете в виду «точность».
Паскаль Куок
Спасибо за ваш вклад, я немного исправил ответ, что-то новое все еще присутствует в последних 2 строках ^^
CoffeDeveloper
27

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

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

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

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

Марк Рэнсом
источник
17
Как отметил другой комментатор, это не соответствует действительности до абсурда; разница может составлять от половины до 10% от стоимости, и если работать в тесном цикле, это приведет к потере многих инструкций для получения дополнительной точности, которая может быть незначительной. Сказать, что вы не должны использовать стандартную FP, когда вы делаете монте-карло, - все равно что сказать, что вы всегда должны использовать самолет, чтобы пересечь страну; он игнорирует многие внешние эффекты. Наконец, это НЕ необычная оптимизация; анализ мертвого кода и сокращение / рефакторинг кода очень распространены.
Алиса
21

На этот вопрос уже есть несколько хороших ответов, но для полноты картины я хотел бы отметить, что применимым разделом стандарта C является 5.1.2.2.3 / 15 (который совпадает с разделом 1.9 / 9 в Стандарт C ++ 11). В этом разделе говорится, что операторы могут быть перегруппированы, только если они действительно ассоциативны или коммутативны.

Rastaban
источник
12

GCC действительно может сделать эту оптимизацию, даже для чисел с плавающей точкой. Например,

double foo(double a) {
  return a*a*a*a*a*a;
}

становится

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

с -O -funsafe-math-optimizations. Это переупорядочение нарушает IEEE-754, поэтому требует флаг.

Целые числа со знаком, как указал Питер Кордес в комментарии, могут выполнить эту оптимизацию без, -funsafe-math-optimizationsтак как она выполняется именно тогда, когда переполнения нет и при переполнении вы получаете неопределенное поведение. Итак, вы получаете

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

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

Чарльз
источник
1
Годболт ссылка с двойным, int и без знака. gcc и clang оптимизируют все три одинаково (с -ffast-math)
Питер Кордес
@PeterCordes Спасибо!
Чарльз