Чому GCC не оптимізує a * a * a * a * a * a to (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має подібну поведінку.

Чому компілятори не розпізнають цей фокус оптимізації?


13
Що означає "розпізнавання pow (a, 6)"?
Varun Madiath

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)в суміш теж. Однакова кількість множень, але, мабуть, більш точна.
Rok Kralj

Відповіді:


2738

Тому що математика з плаваючою точкою не асоціативна . Спосіб групування операндів при множенні плаваючої крапки впливає на числову точність відповіді.

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


10
Так. З -ffast-math він робить таку оптимізацію. Гарна ідея! Але оскільки наш код стосується більшої точності, ніж швидкості, можливо, краще не передати його.
xis

19
IIRC C99 дозволяє компілятору робити такі "небезпечні" оптимізації FP, але GCC (на чому-небудь, крім x87) робить розумну спробу слідувати IEEE 754 - це не "межі помилок"; є лише одна правильна відповідь .
тс.

14
Деталі реалізації powне є ні тут, ні там; ця відповідь навіть не посилається pow.
Стівен Канон

14
@nedR: за замовчуванням ICC дозволяє повторно асоціюватися. Якщо ви хочете отримати стандартну поведінку, вам потрібно встановити -fp-model preciseICC. clangі gccза замовчуванням - сувора відповідність wrt.
Стівен Канон

49
@xis, насправді це -fassociative-mathбуло б неточно; це просто так a*a*a*a*a*aі (a*a*a)*(a*a*a)бувають різні. Справа не в точності; йдеться про відповідність стандартам і строго повторювані результати, наприклад, ті самі результати на будь-якому компіляторі. Числа з плаваючою комою вже не точні. Це рідко недоцільно компілювати -fassociative-math.
Пол Дрейпер

652

Лембдагек правильно вказує, що оскільки асоціативність не відповідає для чисел з плаваючою комою, "оптимізація"a*a*a*a*a*aдо(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: так як я порівнював результати з одинарної точністю, з подвійною точністю суфікси для золотого стандарту - помилка від А обчислених в Двійнику * значно менше , ніж помилка будь-якого з одинарної точності обчислень.
Стівен Канон

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


10
Це точно не те саме. Чангін порядок множення / ділення (виключаючи ділення на 0) безпечніший, ніж порядок зміни суми / віднімання. На мою скромну думку, упорядник повинен спробувати пов’язати mults./divs. тому що це зменшує загальну кількість операцій і, крім підвищення продуктивності, також є збільшенням точності.
CoffeDeveloper

4
@DarioOO: Це не безпечніше. Множення та ділення - це те саме, що і додавання, і віднімання експонента, і зміна порядку може легко призвести до того, що часові компанії перевищують можливий діапазон показника. (Не зовсім те саме, тому що показник не зазнає втрати точності ... але представлення все ще досить обмежене, і переупорядкування може призвести до непредставних значень)
Ben Voigt

8
Я думаю, вам не вистачає певного фону обчислення. Множення і ділення 2 чисел вводить однакову кількість помилок. Хоча віднімання / додавання 2 чисел може ввести більшу помилку, особливо коли 2 числа порядок величин відрізняються, отже, безпечніше переупорядкувати муль / поділ, ніж суб / додати, оскільки це вносить незначну зміну остаточної помилки.
CoffeDeveloper

8
@DarioOO: ризик відрізняється від mul / div: переупорядкування або вносить незначну зміну у кінцевий результат, або показник переповнюється в якийсь момент (де цього не було б раніше), і результат значно відрізняється (потенційно + inf або 0).
Пітер Кордес

@GameDeveloper Надання посилення точності непередбачуваними способами надзвичайно проблематично.
curiousguy

80

Fortran (призначений для наукових обчислень) має вбудований оператор живлення, і наскільки я знаю, компілятори Fortran зазвичай оптимізують підвищення до цілих потужностей аналогічно тому, що ви описуєте. Нажаль, C / C ++ не мають оператора живлення, лише функцію бібліотеки 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
Пошук дерева оптимальної потужності може бути важким, але оскільки це цікаво лише для малих потужностей, очевидною відповіддю є попереднє обчислення його (Knuth надає таблицю до 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, але нічого фантазійного. Вони означають: Прочитати з stdin; використовувати рівень оптимізації O2; вивести лістинг мови складання замість двійкового; в списку повинен використовуватися синтаксис мови збірки Intel; вхід є мовою C (зазвичай мова походить із розширення вхідного файлу, але розширення файлу при читанні з stdin немає); і написати в stdout.

Ось важлива частина результату. Я зазначив це за допомогою коментарів, які вказують на те, що відбувається в мові складання:

; 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

Як зазначали інші афіші, цей варіант не можливий у плаваючій точці, оскільки арифметика з плаваючою комою не асоціативна.


12
Це законно для цілого множення, оскільки переповнення комплементу двох є невизначеним поведінкою. Якщо буде переповнення, воно відбудеться десь, незалежно від операцій упорядкування. Отже, вирази без переповнення оцінюють те саме, вирази, які переповнюють, є невизначеною поведінкою, тому компілятор нормально змінить точку, в якій відбувається переповнення. gcc робить це unsigned intтеж.
Пітер Кордес

51

Тому що 32-бітове число з плаваючою комою - наприклад, 1.024 - не є 1.024. У комп’ютері 1.024 - це інтервал: від (1.024-e) до (1.024 + e), де "e" являє собою помилку. Деякі люди не усвідомлюють цього, а також вважають, що * в а * - це множення довільних точних чисел, без будь-яких помилок, прикріплених до цих чисел. Причина, чому деякі люди не усвідомлюють це, можливо, це математичні обчислення, які вони здійснювали в початкових школах: працюючи лише з ідеальними числами без прикріплених помилок і вважаючи, що добре просто ігнорувати "е", виконуючи множення. Вони не бачать "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 бачить «неозброєним оком».

... як тільки ви дізнаєтеся, який ви програміст , ви можете скористатися перемикачем "-Fast-math", щоб сказати GCC, що "Ей, GCC, я знаю, що я роблю!". Це дозволить GCC конвертувати a * a * a * a * a * a в інший фрагмент тексту - він виглядає відмінним від 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 десяткових цифр).
supercat

23
@supercat: IEEE-754 досить ясно на те , що дані з плаваючою точкою робити представляють точні значення; пункти 3.2 - 3.4 - відповідні розділи. Можна, звичайно, вибрати їх інтерпретувати інакше, так само як ви можете інтерпретувати int x = 3як значення, що xстановить 3 +/- 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одна операція, як би оцінюючи саме за допомогою одного округлення. Наприклад, компілятор може замінити його внутрішньою функцією живлення, яка є і швидшою, і більш точною. Це особливо цікаво, оскільки поведінка частково контролюється програмістом у вихідному коді, тоді як параметри компілятора, надані кінцевим користувачем, іноді можуть використовуватися неправильно.

Стан 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


3
Довготривалі популярні запитання іноді показують свій вік. На це питання було задано і відповіли в 2011 році, коли GCC можна було вибачити за те, що він точно не дотримувався тогочасного стандарту C99. Звичайно зараз це 2014 рік, так що GCC… ах.
Паскаль Куок

Невже ви не відповідаєте на порівняно останні запитання з плаваючою комою без прийнятої відповіді? кашель stackoverflow.com/questions/23703408 кашель
Паскаль Cuoq

Мені здається ... тривожно, що gcc не реалізує C99 з плаваючою точкою.
Девід Монньо

1
Прагми @DavidMonniaux за визначенням необов’язкові для реалізації.
Тім Сегейн

2
@TimSeguine Але якщо прагма не реалізована, її значення за замовчуванням має бути найбільш обмежувальним для реалізації. Я гадаю, саме про це думав Давид. З GCC це тепер виправлено для FP_CONTRACT, якщо використовується режим ISO C : він все ще не реалізує прагму, але в режимі ISO C тепер передбачає, що прагма вимкнена.
vinc17

28

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


3
@greggo Ні, це все ще детерміновано. Жодна випадковість не додається в жодному сенсі цього слова.
Аліса

9
@Alice Здається, досить зрозуміло, що Bjorn тут використовує "детерміновані" в сенсі коду, що дає однаковий результат на різних платформах та різних версіях компілятора тощо (зовнішні змінні, які можуть бути поза контролем програміста) - на відміну від відсутності фактичної числової випадковості під час виконання. Якщо ви вказуєте, що це не належне використання слова, я не збираюся з цим сперечатися.
greggo

5
@greggo За винятком навіть вашої інтерпретації того, що він говорить, все ще неправильно; у цьому вся суть IEEE 754, щоб забезпечити однакові характеристики для більшості (якщо не всіх) операцій на платформах. Тепер він не згадував про платформи чи версії компілятора, що було б поважною проблемою, якщо ви хочете, щоб кожна операція на кожному віддаленому сервері / клієнті була ідентичною .... але це не очевидно з його заяви. Краще слово може бути "надійно схожим" або щось подібне.
Аліса

8
@Alice ти витрачаєш час на всі, включаючи свій власний, аргументуючи семантику. Його значення було зрозумілим.
Ланару

11
@Lanaru Вся суть стандартів IS семантики; його значення було, безумовно, незрозумілим.
Аліса

28

Функції бібліотеки на зразок "pow" зазвичай ретельно розробляються, щоб отримати мінімально можливу помилку (у загальному випадку). Зазвичай це досягається наближенням функцій з сплайнами (згідно з коментарем Паскаля, як видається, найпоширенішою реалізацією є використання алгоритму Remez )

принципово наступна операція:

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". Це було б єдине розумне, що повинен робити постачальник компіляторів.


7
низохідці повинні усвідомити, що ця відповідь ідеально чудова. Я можу навести кілька десятків джерел та документації, які підтверджують мою відповідь, і я, мабуть, більше причетний до точності з плаваючою точкою, ніж будь-який потік. Цілком розумно в StackOverflow додавати пропущену інформацію, яку інші відповіді не охоплюють, тому будьте ввічливі та поясніть свої причини.
CoffeDeveloper

1
Мені здається, що відповідь Стівена Канона охоплює те, що ви маєте сказати. Ви, схоже, наполягаєте на тому, що libms реалізуються за допомогою сплайнів: вони, як правило, використовують скорочення аргументів (залежно від функції, що реалізується) плюс один поліном, коефіцієнти якого отримані більш-менш складними варіантами алгоритму Remez. Плавність в точках стику не вважається ціллю, яку варто переслідувати для функцій libm (якщо вони закінчуються досить точними, вони все одно автоматично є досить гладкими, незалежно від того, на скільки частин був розділений домен).
Паскаль Куок

Друга половина вашої відповіді повністю пропускає те, що компілятори повинні виробляти код, який реалізує те, що говорить вихідний код, періодом. Також ви використовуєте слово "точність", коли маєте на увазі "точність".
Паскаль Куок

Дякую за ваш внесок, я трохи виправив відповідь, щось останнє все ще присутнє в останніх 2 рядках ^^
CoffeDeveloper

27

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

Я здивувався, дізнавшись з інших відповідей, що цей вираз справді можна оптимізувати за допомогою правильних перемикачів компілятора. Або оптимізація є тривіальною, або це крайній випадок набагато більш поширеної оптимізації, або автори-компілятори були надзвичайно ретельними.

Немає нічого поганого в наданні підказки компілятору, як ви робили тут. Це нормальна і очікувана частина процесу мікрооптимізації для перестановки висловлювань та виразів, щоб побачити, які відмінності вони принесуть.

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


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

21

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


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
Godbolt посилання з подвійним, int та непідписаним. gcc і clang оптимізують усі три однаково (з -ffast-math)
Пітер Кордес

@PeterCordes Дякую!
Чарльз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.