Як я можу покращити продуктивність за допомогою високорівневого підходу при впровадженні довгих рівнянь у C ++


92

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

T = (
    mu * (
            pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
            * (
                pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
                - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
            ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l1
            - pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
            - pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
        ) / a
    + K * (l1 * l2 * l3 - 0.1e1) * l2 * l3
) * N1 / l2 / l3

+ (
    mu * (
        - pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
        + pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
        * (
            pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
            - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
        ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l2
        - pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
    ) / a
    + K * (l1 * l2 * l3 - 0.1e1) * l1 * l3
) * N2 / l1 / l3

+ (
    mu * (
        - pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
        - pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
        + pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
        * (
            pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
            - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
        ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l3
    ) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l2
) * N3 / l1 / l2;

Я використовую Maple для генерації коду C ++, щоб уникнути помилок (і заощадити час за допомогою нудної алгебри). Оскільки цей код виконується тисячі (якщо не мільйони) разів, продуктивність викликає занепокоєння. На жаль, наразі математика лише спрощується; довгих рівнянь не уникнути.

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

Я компілюю за допомогою g ++ з --enable-optimize=-O3 .

Оновлення:

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

l1, l2, l3, mu, a, K всі позитивні дійсні числа (не нуль).

Я замінив l1*l2*l3з еквівалентної змінної: J. Це допомогло покращити продуктивність.

Заміна pow(x, 0.1e1/0.3e1)на cbrt(x)було гарною пропозицією.

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


32
Ну, перше, що спадає на думку (якщо компілятор не оптимізує його сам) - це замінити всі ці pow(l1 * l2 * l3, -0.1e1 / 0.3e1) на змінну ... Вам потрібно порівняти свій код, щоб бути впевненим, працює він швидко чи повільно.
SingerOfTheFall

6
Також відформатуйте код, щоб зробити його більш читабельним - це може допомогти у визначенні можливостей для вдосконалення.
Ед Хіл,

26
Чому всі голоси проти і голоси закрити? Для тих з вас, хто не любить числове чи наукове програмування, перегляньте інші запитання. Це хороше питання, яке добре підходить для цього веб-сайту. Сайт scicomp все ще є бета-версією; міграція є невдалим варіантом. Сайт огляду коду не має достатньо очей. Те, що робив ОП, досить часто трапляється в наукових обчисленнях: Побудуйте задачу в символічній математичній програмі, попросіть програму згенерувати код і не торкайтеся результату, оскільки згенерований код - такий безлад.
Девід Хаммен,

6
На веб -сайті @DavidHammen the Code Review недостатньо очей для глядачів - це звучить як проблема з куркою та яйцем та спосіб мислення, який не допомагає CR отримати більше таких очей. Те саме стосується ідеї відмови від бета-сайту scicomp, оскільки це бета-версія - якби всі так думали, єдиним сайтом, який би зростав, був би Stack Overflow.
Матьє Гіндон

13
Це питання обговорюється на мета тут
NathanOliver

Відповіді:


88

Редагувати резюме

  • У моїй оригінальній відповіді було лише зазначено, що код містив багато повторень обчислень і що багато з повноважень включали коефіцієнти 1/3. Наприклад, pow(x, 0.1e1/0.3e1)це те саме, що cbrt(x).
  • Моє друге редагування було просто неправильним, а третє - екстраполяцією цієї неправильності. Саме це змушує людей боятися змінювати результати, подібні до оракулів, із символічних математичних програм, які починаються з літери "М". Я вирішив (тобто наніс ) ці зміни та підсунув їх до кінця поточної редакції цієї відповіді. Однак я їх не видаляв. Я людина. Нам легко помилитися.
  • Моє четверте редагування розробило дуже компактний вираз, який правильно відображає звивистий вираз у питанні, якщо параметри l1,l2 і l3позитивні дійсні числа , а якщоa є ненульовий дійсне число. (Ми все ще не чули від ОП щодо конкретного характеру цих коефіцієнтів. Враховуючи характер проблеми, це обґрунтовані припущення.)
  • Ця редакція намагається відповісти на загальну проблему, як спростити ці вирази.

Насамперед

Я використовую Maple для генерації коду C ++, щоб уникнути помилок.

Клен і Математика іноді пропускають очевидне. Ще важливіше те, що користувачі Maple та Mathematica іноді роблять помилки. Заміна "часто", а може, навіть "майже завжди", замість "іноді", напевно, наближається до позначки.

Ви могли б допомогти Maple спростити цей вираз, розповівши про відповідні параметри. У прикладі на руці, я підозрюю , що l1, l2і l3є позитивними дійсними числами , і що aне є нульовим реальним номером. Якщо це так, скажіть це. Ці символічні математичні програми, як правило, припускають, що наявні величини є складними. Обмеження домену дозволяє програмі робити припущення, які не є дійсними для комплексних чисел.


Як спростити ці великі халепи від символічних математичних програм (це редагування)

Програми символічної математики, як правило, надають можливість надавати інформацію про різні параметри. Використовуйте цю здатність, особливо якщо ваша проблема пов’язана з діленням чи піднесенням до степені. У прикладі , під рукою, ви могли б допомогти Maple спростити цей вираз, кажучи це , що l1, l2і l3є позитивні дійсні числа , і що aне є нульовий реальний номер. Якщо це так, скажіть це. Ці символічні математичні програми, як правило, припускають, що наявні величини є складними. Обмеження домену дозволяє програмі робити такі припущення, як a x b x = (ab) x . Це лише тоді, коли aі bє додатними дійсними числами і якщоx є дійсними. Він не дійсний у комплексних числах.

Зрештою, ці символічні математичні програми слідують алгоритмам. Допоможіть. Спробуйте пограти в розширення, збір та спрощення, перш ніж створювати код. У цьому випадку ви могли б зібрати такі терміни, що включають коефіцієнт, muі такі, що включають коефіцієнтK . Зведення виразу до його «найпростішої форми» залишається трохи мистецтвом.

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


Про конкретне питання

Ви робите багато додавання та віднімання в цьому обчисленні. Ви можете потрапити в глибокі неприємності, якщо у вас є умови, які майже скасовують одне одного. Ви витрачаєте багато CPU, якщо у вас є один термін, який домінує над іншими.

Далі ви витрачаєте багато процесора, виконуючи повторні обчислення. Якщо ви не ввімкнули це -ffast-math, що дозволяє компілятору порушувати деякі правила IEEE з плаваючою комою, компілятор не буде (насправді, не повинен) спрощувати цей вираз для вас. Натомість воно зробить саме те, що ви йому наказали. Як мінімум, l1 * l2 * l3перед обчисленням цього безладу слід розрахувати .

Нарешті, ви телефонуєте багато pow, що надзвичайно повільно. Зверніть увагу, що кілька із цих дзвінків мають вигляд (l1 * l2 * l3) (1/3) . Багато з цих дзвінків powможна виконати одним дзвінком std::cbrt:

l123 = l1 * l2 * l3;
l123_pow_1_3 = std::cbrt(l123);
l123_pow_4_3 = l123 * l123_pow_1_3;

З цим,

  • X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)стає X * l123_pow_1_3.
  • X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)стає X / l123_pow_1_3.
  • X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)стає X * l123_pow_4_3.
  • X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)стає X / l123_pow_4_3.


Клен справді пропустив очевидне.
Наприклад, є набагато простіший спосіб писати

(pow(l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

Якщо припустити , що l1, l2і l3реально , а не комплексні чисел, що корінь реального куба (а не принцип комплексного кореня) повинні бути витягнутий, вище , зводиться до

2.0/(3.0 * pow(l1 * l2 * l3, 1.0/3.0))

або

2.0/(3.0 * l123_pow_1_3)

Використовуючи cbrt_l123замість l123_pow_1_3, неприємний вираз у питанні зводиться до

l123 = l1 * l2 * l3; 
cbrt_l123 = cbrt(l123);
T = 
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);

Завжди переглядайте, але завжди також спрощуйте.


Ось декілька моїх кроків для досягнення вищезазначеного:

// Step 0: Trim all whitespace.
T=(mu*(pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l1-pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l1/0.3e1-pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l1/0.3e1)/a+K*(l1*l2*l3-0.1e1)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l2/0.3e1+pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l2-pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l2/0.3e1)/a+K*(l1*l2*l3-0.1e1)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l3/0.3e1-pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l3/0.3e1+pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l3)/a+K*(l1*l2*l3-0.1e1)*l1*l2)*N3/l1/l2;

// Step 1:
//   l1*l2*l3 -> l123
//   0.1e1 -> 1.0
//   0.4e1 -> 4.0
//   0.3e1 -> 3
l123 = l1 * l2 * l3;
T=(mu*(pow(l1*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l1-pow(l2*pow(l123,-1.0/3),a)*a/l1/3-pow(l3*pow(l123,-1.0/3),a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l2/3+pow(l2*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l2-pow(l3*pow(l123,-1.0/3),a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l3/3-pow(l2*pow(l123,-1.0/3),a)*a/l3/3+pow(l3*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 2:
//   pow(l123,1.0/3) -> cbrt_l123
//   l123*pow(l123,-4.0/3) -> pow(l123,-1.0/3)
//   (pow(l123,-1.0/3)-pow(l123,-1.0/3)/3) -> 2.0/(3.0*cbrt_l123)
//   *pow(l123,-1.0/3) -> /cbrt_l123
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T=(mu*(pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1-pow(l2/cbrt_l123,a)*a/l1/3-pow(l3/cbrt_l123,a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l2/3+pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2-pow(l3/cbrt_l123,a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l3/3-pow(l2/cbrt_l123,a)*a/l3/3+pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 3:
//   Whitespace is nice.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)*a/l1/3
       -pow(l3/cbrt_l123,a)*a/l1/3)/a
   +K*(l123-1.0)*l2*l3)*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l2/3
       +pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)*a/l2/3)/a
   +K*(l123-1.0)*l1*l3)*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l3/3
       -pow(l2/cbrt_l123,a)*a/l3/3
       +pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a
   +K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 4:
//   Eliminate the 'a' in (term1*a + term2*a + term3*a)/a
//   Expand (mu_term + K_term)*something to mu_term*something + K_term*something
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +K*(l123-1.0)*l2*l3*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +K*(l123-1.0)*l1*l3*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l3))*N3/l1/l2
 +K*(l123-1.0)*l1*l2*N3/l1/l2;

// Step 5:
//   Rearrange
//   Reduce l2*l3*N1/l2/l3 to N1 (and similar)
//   Reduce 2.0/(3.0*cbrt_l123)*cbrt_l123/l1 to 2.0/3.0/l1 (and similar)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/3.0/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/3.0/l3))*N3/l1/l2
 +K*(l123-1.0)*N1
 +K*(l123-1.0)*N2
 +K*(l123-1.0)*N3;

// Step 6:
//   Factor out mu and K*(l123-1.0)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*(  ( pow(l1/cbrt_l123,a)*2.0/3.0/l1
         -pow(l2/cbrt_l123,a)/l1/3
         -pow(l3/cbrt_l123,a)/l1/3)*N1/l2/l3
      + (-pow(l1/cbrt_l123,a)/l2/3
         +pow(l2/cbrt_l123,a)*2.0/3.0/l2
         -pow(l3/cbrt_l123,a)/l2/3)*N2/l1/l3
      + (-pow(l1/cbrt_l123,a)/l3/3
         -pow(l2/cbrt_l123,a)/l3/3
         +pow(l3/cbrt_l123,a)*2.0/3.0/l3)*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 7:
//   Expand
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1*N1/l2/l3
      -pow(l2/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l3/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l1/cbrt_l123,a)/l2/3*N2/l1/l3
      +pow(l2/cbrt_l123,a)*2.0/3.0/l2*N2/l1/l3
      -pow(l3/cbrt_l123,a)/l2/3*N2/l1/l3
      -pow(l1/cbrt_l123,a)/l3/3*N3/l1/l2
      -pow(l2/cbrt_l123,a)/l3/3*N3/l1/l2
      +pow(l3/cbrt_l123,a)*2.0/3.0/l3*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 8:
//   Simplify.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);


Неправильна відповідь, навмисно дотримана для смирення

Зверніть увагу, що це вражено. Це неправильно.

Оновлення

Клен справді пропустив очевидне. Наприклад, є набагато простіший спосіб писати

(порошок (l1 * l2 * l3, -0,1e1 / 0,3e1) - l1 * l2 * l3 * порошок (l1 * l2 * l3, -0,4e1 / 0,3e1) / 0,3e1)

Якщо припустити , що l1, l2іl3 реально , а не комплексні чисел, а також про те , що дійсний корінь куби (а не принцип комплексного корінь) повинні бути витягнутий, вище , зводиться до нуля. Цей розрахунок нуля повторюється багато разів.

Друге оновлення

Якщо я правильно розрахував математику (немає жодної гарантії, що я зробив математику правильно), неприємний вираз у питанні зводиться до

l123 = l1 * l2 * l3; 
cbrt_l123_inv = 1.0 / cbrt(l123);
nasty_expression =
    K * (l123 - 1.0) * (N1 + N2 + N3) 
    - (  pow(l1 * cbrt_l123_inv, a) * (N2 + N3) 
       + pow(l2 * cbrt_l123_inv, a) * (N1 + N3) 
       + pow(l3 * cbrt_l123_inv, a) * (N1 + N2)) * mu / (3.0*l123);

Вище , передбачає , що l1, l2і l3позитивні дійсні числа.


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

3
@Deduplicator - не з плаваючою комою. Якщо ніхто не дозволить небезпечну математичну оптимізацію (наприклад, вказавши -ffast-mathgcc або clang), компілятор не може покладатися на pow(x,-1.0/3.0)рівність x*pow(x,-4.0/3.0). Останній може занепасти, тоді як перший ні. Щоб відповідати стандарту з плаваючою комою, компілятор не повинен оптимізувати цей розрахунок до нуля.
Девід Хаммен,

Ну, це набагато амбітніше за все, що я мав на увазі.
Дедулікатор

1
@Deduplicator: Як я коментував іншу відповідь : Вам потрібні -fno-math-errnoоднакові powвиклики g ++ до CSE . (Хіба що, можливо, це може довести, що Поу не потрібно буде встановлювати помилки?)
Пітер Кордес,

1
@Lefti - Взяти багато за відповідь Вальтера. Це набагато швидше. З усіма цими відповідями існує потенційна проблема, пов’язана з числовим скасуванням. Якщо припустити, що ваш N1, N2і N3є негативним, один із них 2*N_i-(N_j+N_k)буде негативним, один буде позитивним, а інший буде десь посередині. Це може легко призвести до проблем із числовим скасуванням.
Девід Хаммен,

32

Перше, на що слід звернути увагу, powце дійсно дорого, тому вам слід позбутися цього якомога більше. Переглядаючи вираз, я бачу багато повторень pow(l1 * l2 * l3, -0.1e1 / 0.3e1)і pow(l1 * l2 * l3, -0.4e1 / 0.3e1). Тож я би очікував великого виграшу від попередніх обчислень:

 const double c1 = pow(l1 * l2 * l3, -0.1e1 / 0.3e1);
const double c2 = boost::math::pow<4>(c1);

де я використовую boost pow .

Крім того, у вас є ще кілька powз експонентою a. Якщо aце Integer і відомо на час компіляції, ви також можете замінити такі, boost::math::pow<a>(...)щоб отримати подальшу продуктивність. Я також хотів би запропонувати , щоб замінити такі терміни , як a / l1 / 0.3e1зa / (l1 * 0.3e1) , як множення швидше , ніж розподіл.

Нарешті, якщо ви використовуєте g ++, ви можете використовувати -ffast-mathпрапор, який дозволяє оптимізатору бути більш агресивним при перетворенні рівнянь. Прочитайте про те, що насправді робить цей прапор , оскільки він має побічні ефекти.


5
У нашому коді використання -ffast-mathпризводить до того, що код стає нестабільним або дає чіткі помилкові відповіді. У нас схожа проблема з компіляторами Intel, і ми повинні використовувати цю -fp-model preciseопцію, інакше код або підірве, або дасть неправильні відповіді. Отже, це -ffast-mathможе пришвидшити, але я рекомендую діяти дуже обережно з цим варіантом, на додаток до побічних ефектів, перелічених у вашому запитанні.
tpg2114

2
@ tpg2114: Згідно з моїм тестуванням, вам потрібно лише,-fno-math-errno щоб g ++ міг підняти однакові виклики powз циклу. Це найменш "небезпечна" частина математичної математики, для більшості коду.
Пітер Кордес,

1
@PeterCordes Це цікаві результати! У нас також були проблеми з pow надзвичайною повільністю, і ми в кінцевому підсумку використали dlsymзлом, згаданий у коментарях, щоб отримати значне підвищення продуктивності, коли насправді ми могли робити це з трохи меншою точністю.
tpg2114

Чи не зрозуміє GCC, що pow - це чиста функція? Це, мабуть, вбудовані знання.
usr

6
@usr: Гадаю, справа лише в цьому. powце НЕ чиста функція, в відповідності зі стандартом, так як передбачається набір errnoв деяких обставинах. Установка прапори , такі як -fno-math-errnoпричина, щоб це НЕ встановлено errno(тим самим порушивши стандарт), але тоді це чиста функція , і може бути оптимізована як такі.
Nate Eldredge

20

Ого, який чортовий вираз. Створення виразу за допомогою Maple насправді було неоптимальним вибором тут. Результат просто нечитабельний.

  1. обрали імена змінних, що говорять (не l1, l2, l3, а, наприклад, висота, ширина, глибина, якщо це те, що вони означають). Тоді вам легше зрозуміти власний код.
  2. обчислюйте субтерми, які ви використовуєте кілька разів, заздалегідь і зберігайте результати у змінних із іменами, що говорять.
  3. Ви згадуєте, що вираз оцінюється дуже багато разів. Думаю, у внутрішньому циклі змінюється лише декілька параметрів. Обчислити всі інваріантні підтерми перед цим циклом. Повторіть для другої внутрішньої петлі і так далі, поки всі інваріанти не опиняться поза петлею.

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


8
"компілятор повинен це робити, але іноді цього не робить", тут ключове значення. крім читабельності, звичайно.
Хав'єр

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

4
Повторно вибирайте імена змінних, що говорять - Багато разів це приємне правило не застосовується, коли ви робите математику. Розглядаючи код, який повинен впроваджувати алгоритм у науковому журналі, я б набагато бачив, щоб символи в коді були саме тими, що використовуються в журналі. Як правило, це означає надзвичайно короткі імена, можливо, з нижчим індексом.
Девід Хаммен,

8
"Результат просто нечитабельний" - чому це проблема? Вам би було байдуже, що високий рівень виводу мови з генератора лексерів чи аналізаторів був "нечитабельним" (людиною). Тут важливо те, що вхідні дані у генератор коду (Maple) читаються та перевіряються. Що не слід робити, це редагувати згенерований код вручну, якщо ви хочете бути впевненими, що він не містить помилок.
alephzero

3
@DavidHammen: Ну, в такому випадку однобуквеними є "імена, що говорять". Наприклад, виконуючи геометрію у двовимірній декартовій системі координат, xі неy є безглуздими однобуквеними змінними, це цілі слова з точним визначенням та добре і широко зрозумілим значенням.
Jörg W Mittag

17

Відповідь Девіда Хаммена хороша, але все ще далека від оптимальної. Продовжимо з його останнім виразом (на момент написання цього)

auto l123 = l1 * l2 * l3;
auto cbrt_l123 = cbrt(l123);
T = mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                   + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                   + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
  + K*(l123-1.0)*(N1+N2+N3);

які можна додатково оптимізувати. Зокрема, ми можемо уникнути виклику cbrt()та одного із викликів, pow()якщо використовуємо деякі математичні ідентичності. Зробимо це ще раз крок за кроком.

// step 1 eliminate cbrt() by taking the exponent into pow()
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a; // avoid division
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)*pow(l1*l1/(l2*l3),athird)
                   + (N2+N2-N3-N1)*pow(l2*l2/(l1*l3),athird)
                   + (N3+N3-N1-N2)*pow(l3*l3/(l1*l2),athird))
  + K*(l123-1.0)*(N1+N2+N3);

Зауважте, що я також оптимізував 2.0*N1і N1+N1т. Д. Далі ми можемо обійтися лише двома викликами pow().

// step 2  eliminate one call to pow
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a;
auto pow_l1l2_athird = pow(l1/l2,athird);
auto pow_l1l3_athird = pow(l1/l3,athird);
auto pow_l2l3_athird = pow_l1l3_athird/pow_l1l2_athird;
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)* pow_l1l2_athird*pow_l1l3_athird
                   + (N2+N2-N3-N1)* pow_l2l3_athird/pow_l1l2_athird
                   + (N3+N3-N1-N2)/(pow_l1l3_athird*pow_l2l3_athird))
  + K*(l123-1.0)*(N1+N2+N3);

Оскільки дзвінки до нас pow()на сьогоднішній день є найдорожчою операцією, варто зменшити їх наскільки це можливо (наступною дорогою операцією був виклик cbrt(), який ми усунули).

Якщо випадково aціле число, виклики до powможуть бути оптимізовані до викликів до cbrt(плюс цілі повноваження), або якщо athirdце напівціле число, ми можемо використовувати sqrt(плюс цілі повноваження). Крім того, якщо випадково l1==l2або l1==l3або l2==l3одного або обох викликів powможе бути усунена. Отже, варто розглядати це як особливі випадки, якщо такі шанси реально існують.


@gnat Я вдячний вам за редагування (я думав зробити це сам), але вважав би це справедливішим, якби відповідь Девіда також посилалася на цю. Чому ви також не редагуєте відповідь Девіда подібним чином?
Вальтер

1
Я редагував лише тому, що бачив, як ви прямо про це згадували; Я перечитав відповідь Девіда і не зміг знайти там посилання на вашу відповідь. Я намагаюся уникати редагувань, де не на 100% зрозуміло, що матеріали, які я додаю, відповідають намірам автора
gnat

1
@Walter - Моя відповідь тепер посилається на вашу.
Девід Хаммен,

1
Це точно не я. Я підтримав вашу відповідь кілька днів тому. Я також отримав випадковий прогін проти моєї відповіді. Інколи просто трапляються речі.
Девід Хаммен,

1
Ми з вами отримали мізерний голос проти кожного. Подивіться на всі голоси проти цього питання! На даний момент питання отримало 16 голосів проти. Він також отримав 80 голосів проти, що з лишком компенсувало всіх цих голосів.
Девід Хаммен,

12
  1. Скільки це "багато багато"?
  2. Скільки часу це займає?
  3. Чи змінюються ВСІ параметри між перерахунками цієї формули? Або ви можете кешувати деякі заздалегідь обчислені значення?
  4. Я намагався спростити цю формулу вручну, хотів би знати, чи вона щось економить?

    C1 = -0.1e1 / 0.3e1;
    C2 =  0.1e1 / 0.3e1;
    C3 = -0.4e1 / 0.3e1;
    
    X0 = l1 * l2 * l3;
    X1 = pow(X0, C1);
    X2 = pow(X0, C2);
    X3 = pow(X0, C3);
    X4 = pow(l1 * X1, a);
    X5 = pow(l2 * X1, a);
    X6 = pow(l3 * X1, a);
    X7 = a / 0.3e1;
    X8 = X3 / 0.3e1;
    X9 = mu / a;
    XA = X0 - 0.1e1;
    XB = K * XA;
    XC = X1 - X0 * X8;
    XD = a * XC * X2;
    
    XE = X4 * X7;
    XF = X5 * X7;
    XG = X6 * X7;
    
    T = (X9 * ( X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
      + (X9 * (-XE + X5 * XD - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
      + (X9 * (-XE - XF + X6 * XD) / l3 + XB * l1 * l2) * N3 / l1 / l2;

[ДОБАВЛЕНО] Я ще трохи попрацював над останньою формулою з трьох рядків і дійшов до такої краси:

T = X9 / X0 * (
      (X4 * XD - XF - XG) * N1 + 
      (X5 * XD - XE - XG) * N2 + 
      (X5 * XD - XE - XF) * N3)
  + XB * (N1 + N2 + N3)

Дозвольте мені поетапно показати свою роботу:

T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / l1 / l2;


T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / (l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / (l1 * l3) 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / (l1 * l2);

T = (X9 * (X4 * XD - XF - XG) + XB * l1 * l2 * l3) * N1 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) + XB * l1 * l2 * l3) * N2 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XF) + XB * l1 * l2 * l3) * N3 / (l1 * l2 * l3);

T = (X9 * (X4 * XD - XF - XG) + XB * X0) * N1 / X0 
  + (X9 * (X5 * XD - XE - XG) + XB * X0) * N2 / X0 
  + (X9 * (X5 * XD - XE - XF) + XB * X0) * N3 / X0;

T = X9 * (X4 * XD - XF - XG) * N1 / X0 + XB * N1 
  + X9 * (X5 * XD - XE - XG) * N2 / X0 + XB * N2
  + X9 * (X5 * XD - XE - XF) * N3 / X0 + XB * N3;


T = X9 * (X4 * XD - XF - XG) * N1 / X0 
  + X9 * (X5 * XD - XE - XG) * N2 / X0
  + X9 * (X5 * XD - XE - XF) * N3 / X0
  + XB * (N1 + N2 + N3)

2
Це помітно, так? :) FORTRAN, IIRC, був розроблений для ефективних розрахунків формул ("FOR" - для формули).
Влад Файнштейн

Більшість кодів F77, які я бачив, виглядали так (наприклад, BLAS & NR). Дуже радий, що Fortran 90-> 2008 існує :)
Кайл Канос

Так. Якщо ви перекладаєте формулу, який кращий спосіб, ніж FORmulaTRANslation?
Brian Drummond

1
Ваша "оптимізація" атакує не там, де. Дорогі біти - це дзвінки std::pow(), яких ви все ще маєте в 6, 3 рази більше, ніж потрібно. Іншими словами, ваш код у 3 рази повільніший, ніж можливо.
Вальтер

7

Це може бути трохи стисло, але я насправді знайшов хороший пришвидшення для поліномів (інтерполяція енергетичних функцій) за допомогою форми Горнера, яка в основному переписує ax^3 + bx^2 + cx + dяк d + x(c + x(b + x(a))). Це дозволить уникнути багатьох повторних дзвінків pow()і зупинить вас від безглуздих дій, таких як окремі дзвінки pow(x,6)та pow(x,7)замість того, щоб просто робити x*pow(x,6).

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

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


Форма Горнера є досить стандартною для кодування багаточленів, і це взагалі не має відношення до питання.
Вальтер

1
Це може бути правдою, враховуючи його приклад, але ви помітите, що він сказав "рівняння цього типу". Я вважав, що відповідь буде корисною, якщо в плакаті є якісь поліноми в його системі. Я особливо помітив, що генератори коду для програм CAS, таких як Mathematica та Maple, як правило, НЕ дають вам форму Горнера, якщо ви про це спеціально не попросите; вони за замовчуванням відповідають тому, як ви зазвичай пишете поліном як людину.
neocpp

3

Схоже, у вас багато повторюваних операцій.

pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
pow(l1 * l2 * l3, -0.4e1 / 0.3e1)

Ви можете попередньо розрахувати їх, щоб не повторювати виклик powфункції, яка може бути дорогою.

Ви також можете попередньо обчислити

l1 * l2 * l3

оскільки ви використовуєте цей термін неодноразово.


6
Б'юся об заклад, оптимізатор вже робить це за вас ... хоча це робить принаймні код більш читабельним.
Каролі Горват

Я зробив це, але це зовсім не пришвидшило справи. Я зрозумів, що це тому, що оптимізація компілятора вже подбала про це.

зберігання l1 * l2 * l3 все-таки пришвидшує процес, не впевнений, чому з оптимізацією компілятора

оскільки компілятор іноді просто не може виконати певні оптимізації або виявити їх у конфлікті з іншими параметрами.
Хав'єр

1
Насправді компілятор не повинен виконувати ці оптимізації, якщо -ffast-mathце не ввімкнено, і, як зазначено в коментарі @ tpg2114, ця оптимізація може створити надзвичайно нестабільні результати.
Девід Хаммен,

0

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

https://developer.nvidia.com/how-to-cuda-c-cpp

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


10
Ця відповідь ортогональна розглядуваному питанню. Хоча графічні процесори мають багато-багато процесорів, вони досить повільні в порівнянні з FPU, вбудованим у центральний процесор. Виконання одного послідовного обчислення з графічним процесором - велика втрата. Процесор повинен заповнити конвеєр до графічного процесора, почекати, поки повільний графічний процесор виконає це єдине завдання, а потім вивантажити результат. Незважаючи на те, що графічні процесори абсолютно фантастичні, коли проблема, що розглядається, є паралельною, вони абсолютно жорстокі, коли йдеться про виконання послідовних завдань.
Девід Хаммен,

1
В оригінальному запитанні: "Оскільки цей код виконується багато разів, продуктивність викликає занепокоєння.". Це більше, ніж "багато". Оператор може надсилати обчислення різьбовим способом.
user3791372

0

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

Немислимо (ризикуючи бути поза темою?), Що ви можете використовувати python з numpy та / або scipy. Наскільки це можливо, ваші розрахунки можуть бути більш читабельними.


0

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

  • Колекція компілятора GNU є безкоштовною, гнучкою та доступною для багатьох архітектур
  • Компілятори Intel дуже швидкі, дуже дорогі і можуть також дати хороші результати для архітектур AMD (я вважаю, що існує академічна програма)
  • Компілятори Clang швидкі, безкоштовні та можуть привести до результатів, подібних до GCC (деякі люди кажуть, що вони швидші, кращі, але це може відрізнятися в кожному конкретному випадку, я пропоную зробити свій власний досвід)
  • PGI (група Portland) не є безкоштовною як компілятори Intel.
  • Компілятори PathScale можуть досягти хороших результатів на архітектурах AMD

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

Щасти!

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