Как повысить производительность с помощью высокоуровневого подхода при реализации длинных уравнений в С++

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

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 для генерации кода на С++, чтобы избежать ошибок (и сэкономить время с утомительной алгеброй). Поскольку этот код выполняется тысячи (если не миллионы) раз, производительность является проблемой. К сожалению, математика только упрощает до сих пор; длинные уравнения неизбежны.

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

Я компилирую с помощью g++ с --enable-optimize=-O3.

Update:

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

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

Я заменил l1*l2*l3 эквивалентной переменной: J. Это помогло повысить производительность.

Замена pow(x, 0.1e1/0.3e1) на cbrt(x) была хорошим предложением.

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

Ответ 1

Редактировать резюме

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

Сначала первые вещи

Я использую Maple для генерации кода на С++, чтобы избежать ошибок.

Клен и Математика иногда пропускают очевидное. Еще важнее то, что пользователи Maple и Mathematica иногда ошибаются. Подставляя "часто" или, возможно, даже "почти всегда", вместо "иногда, вероятно, ближе к знаку".

Вы могли бы помочь Maple упростить это выражение, рассказав ему о соответствующих параметрах. В приведенном примере я подозреваю, что l1, l2 и l3 являются положительными действительными числами и что a является ненулевым вещественным числом. Если это так, скажите это. Эти символические математические программы обычно предполагают, что имеющиеся величины являются сложными. Ограничение домена позволяет программе делать допущения, которые недопустимы в комплексных числах.


Как упростить эти большие беспорядки из символических математических программ (это редактирование)

Символьные математические программы обычно предоставляют возможность предоставлять информацию о различных параметрах. Используйте эту способность, особенно если ваша проблема связана с делением или возведением в степень. В приведенном примере вы могли бы помочь Maple упростить это выражение, сообщив ему, что l1, l2 и l3 являются положительными действительными числами и что a является ненулевым вещественным числом. Если это так, скажите это. Эти символические математические программы обычно предполагают, что имеющиеся величины являются сложными. Ограничение домена позволяет программе делать предположения, такие как x b x= (ab) x. Это только если a и b - положительные действительные числа, а если x вещественное. Он недействителен в комплексных числах.

В конечном счете, эти символические математические программы следуют алгоритмам. Помогите ему. Попробуйте играть с расширением, сбором и упрощением перед созданием кода. В этом случае вы могли бы собрать эти термины с коэффициентом mu и с коэффициентом K. Сокращение выражения до его "простейшей формы" остается немного искусством.

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


Об определенном вопросе

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

Затем вы тратите много 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);


Неверный ответ, намеренно сдержанный

Обратите внимание, что это поражено. Это неправильно.

Обновление

Клен пропустил очевидное. Например, гораздо проще писать

(pow (l1 * l2 * l3, -0.1e1/0.3e1) - l1 * l2 * l3 * pow (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 - положительные действительные числа. Забастовкa >

Ответ 2

Прежде всего следует отметить, что 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, который позволяет оптимизатору быть более агрессивным в преобразовании уравнений. Прочитайте о том, что на самом деле делает этот флаг, поскольку он имеет побочные эффекты.

Ответ 3

Воа, какое чертов выражение. Создание выражения с помощью Maple на самом деле было субоптимальным выбором здесь. Результат просто нечитабель.

  • выбрали имена переменных имен (не l1, l2, l3, но, например, высота, ширина, глубина, если это то, что они означают). Тогда вам легче понять свой собственный код.
  • рассчитать субтермы, которые вы используете несколько раз, заранее и сохраняете результаты в переменных с именами говорящих.
  • Вы отмечаете, что выражение оценивается очень много раз. Я думаю, только несколько параметров меняются во внутреннем большинстве циклов. Вычислить все инвариантные подтермы перед этим циклом. Повторите для второго внутреннего цикла и так далее, пока все инварианты не будут за пределами цикла.

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

Ответ 4

Ответ Дэвида Хаммена хорош, но все же далек от оптимального. Продолжим его последнее выражение (на момент написания)

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. Поэтому стоит рассматривать их как особые случаи, если такие шансы реально существуют.

Ответ 5

  • Сколько "многого"?
  • Сколько времени займет?
  • Изменены ли ВСЕ параметры между пересчетом этой формулы? Или вы можете кэшировать некоторые предварительно рассчитанные значения?
  • Я попытался вручную упростить эту формулу, хотел бы знать, сохраняет ли она что-нибудь?

    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;
    

[ADDED] Я работал над последней трехстрочной формулой и довел ее до этой красоты:

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)

Ответ 6

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

Это не относится непосредственно к вашей текущей проблеме, но если у вас есть полиномы высокого порядка с целыми степенями, это может помочь. Возможно, вам придется следить за численной стабильностью и проблемами переполнения, поскольку для этого важна последовательность операций (хотя в целом я действительно считаю, что для этого помогает Horner Form, поскольку x^20 и x обычно на много порядков).

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

Ответ 7

Похоже, что у вас много повторяющихся операций.

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

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

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

l1 * l2 * l3

поскольку вы используете этот термин повторно.

Ответ 8

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

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

Если нет, вы можете рассмотреть несколько потоков для вычислений.

Ответ 9

Если бы вы случайно поставили расчет. Если есть векторные операции, вы можете действительно исследовать использование blas или lapack, которое в некоторых случаях может запускать операции параллельно.

Можно ли (рискуя быть вне темы?), что вы могли бы использовать python с numpy и/или scipy. В той мере, в какой это возможно, ваши расчеты могут быть более читаемыми.

Ответ 10

Поскольку вы явно задавали вопрос о оптимизации высокого уровня, возможно, стоит попробовать разные компиляторы С++. В настоящее время компиляторы - это очень сложные оптимизаторы, а производители процессоров могут реализовывать очень мощные и конкретные оптимизации. Но учтите, что некоторые из них не являются бесплатными (но может быть бесплатная академическая программа).

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

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

Удачи!