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

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

TylerH
источник
32
Ну, первое, что приходит в голову (если только компилятор не оптимизирует его сам), - это заменить все те pow(l1 * l2 * l3, -0.1e1 / 0.3e1)переменными ... Вам нужно протестировать свой код, чтобы убедиться, работает он быстро или медленно.
SingerOfTheFall
6
Также отформатируйте код, чтобы сделать его более читабельным - это может помочь в определении возможностей для улучшения.
Эд Хил
26
Почему все голоса против и закрытие? Для тех из вас, кто не любит числовое или научное программирование, посмотрите другие вопросы. Это хороший вопрос, который хорошо подходит для этого сайта. Сайт scicomp все еще находится в стадии бета-тестирования; миграция там не лучший вариант. Сайту обзора кода не хватает sciomp eyes. То, что сделал OP, довольно часто случается в научных вычислениях: создайте задачу в символьной математической программе, попросите программу сгенерировать код и не трогайте результат, потому что сгенерированный код представляет собой такой беспорядок.
Дэвид Хаммен,
6
@DavidHammen, на сайте Code Review не хватает глаз sciomp - звучит как проблема курицы и яйца, и мышление, которое не помогает CR получить больше таких глаз. То же самое относится и к идее отказа от бета-сайта scicomp, потому что это бета - если бы все так думали, единственным сайтом, который мог бы развиваться, был бы Stack Overflow.
Mathieu Guindon
13
Этот вопрос обсуждается здесь
Натан Оливер

Ответы:

88

Редактировать сводку

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

Перво-наперво

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

Maple и Mathematica иногда упускают из виду очевидное. Что еще более важно, пользователи Maple и Mathematica иногда делают ошибки. Замена «часто» или, может быть, даже «почти всегда» вместо «иногда», вероятно, ближе к цели.

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


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

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

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

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


О конкретном вопросе

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

Затем вы тратите много ресурсов ЦП, выполняя повторяющиеся вычисления. Если вы не включили -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являются положительными действительными числами.

Дэвид Хаммен
источник
2
Что ж, устранение CSE должно работать, независимо от ослабленной семантики (и OP, указанный в комментариях, это работает). Хотя, конечно, если это имеет значение (измеряется), это следует проверить (сгенерированная сборка). Ваши замечания о доминирующих терминах, пропущенных упрощениях формул, более специализированных функциях и опасностях отмены очень хороши.
Дедупликатор
3
@Deduplicator - не с плавающей точкой. Если не включить небезопасные математические оптимизации (например, указав -ffast-mathс помощью gcc или clang), компилятор не может полагаться на pow(x,-1.0/3.0)равенство x*pow(x,-4.0/3.0). Последний может быть недостаточным, а первый - нет. Чтобы соответствовать стандарту с плавающей запятой, компилятор не должен оптимизировать это вычисление до нуля.
Дэвид Хаммен
Что ж, это намного более амбициозно, чем я имел в виду.
Дедупликатор
1
@Deduplicator: Как я прокомментировал другой ответ : вам нужны -fno-math-errnoидентичные powвызовы g ++ для CSE . (Если, может быть, он не докажет, что pow не нужно устанавливать errno?)
Питер Кордес,
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);

где я использую функцию повышения мощности .

Кроме того, у вас есть еще несколько powс показателем степени a. Если aэто целое число и известно во время компиляции, вы также можете заменить их на 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 02
2
@ tpg2114: Согласно моим-fno-math-errno тестам , вам нужно только, чтобы g ++ мог выводить идентичные вызовы powиз цикла. Это наименее «опасная» часть -ffast-math для большей части кода.
Питер Кордес
1
@PeterCordes Это интересные результаты! У нас также были проблемы с тем, pow что мы были очень медленными, и в конечном итоге мы использовали dlsymхак, упомянутый в комментариях, для значительного увеличения производительности, когда на самом деле мы могли работать с немного меньшей точностью.
tpg2114 02
Разве GCC не понял бы, что pow - это чистая функция? Вероятно, это встроенные знания.
usr
6
@usr: Думаю, в том-то и дело. powэто не чистая функция, в соответствии со стандартом, так как предполагается набор errnoв некоторых обстоятельствах. Установка флагов, например, -fno-math-errnoзаставляет ее не устанавливаться errno(тем самым нарушая стандарт), но тогда это чистая функция и может быть оптимизирована как таковая.
Нейт Элдридж,
20

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

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

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

cdonat
источник
8
«компилятор должен это делать, но иногда это не так», - вот ключевой момент. помимо удобочитаемости, конечно.
Хавьер
3
Если от компилятора не требуется что-то делать, то это предположение почти всегда неверно.
edmz 02
4
Повторно выбирайте говорящие имена переменных. Часто это хорошее правило не применяется, когда вы занимаетесь математикой. Когда я смотрю на код, который должен реализовывать алгоритм в научном журнале, я бы предпочел видеть в коде именно те символы, которые используются в журнале. Обычно это означает очень короткие имена, возможно, с нижним индексом.
Дэвид Хаммен
8
«Результат просто нечитаем» - почему это проблема? Вам было бы все равно, что вывод на языке высокого уровня из лексера или синтаксического анализатора был «нечитаемым» (людьми). Здесь важно то, что входные данные генератора кода (Maple) можно читать и проверять. Чего не следует делать, так это редактировать сгенерированный код вручную, если вы хотите быть уверены, что он не содержит ошибок.
alephzero 02
3
@DavidHammen: Ну, в этом случае однобуквенные имена - это «говорящие имена». Например, при выполнении геометрии в 2-мерной декартовой системе координат 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);

Поскольку вызовы to 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. Do ВСЕХ параметров изменяются между перерасчетом этой формулы? Или вы можете кэшировать заранее рассчитанные значения?
  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?
Брайан Драммонд,
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 перед генерацией кода может привести к огромной разнице.

neocpp
источник
Форма Хорнера довольно стандартна для кодирования многочленов, и это не имеет никакого отношения к вопросу.
Уолтер
1
Это может быть правдой на его примере, но вы заметите, что он сказал «уравнения этого типа». Я подумал, что ответ был бы полезен, если бы у плаката были какие-то полиномы в его системе. Я особенно заметил, что генераторы кода для программ CAS, таких как Mathematica и Maple, обычно НЕ предоставляют вам форму Хорнера, если вы специально не попросите об этом; по умолчанию они соответствуют тому, как вы обычно пишете многочлен как человек.
neocpp 05
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

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

user3791372
источник
10
Этот ответ ортогонален поставленному вопросу. Хотя у графических процессоров много-много процессоров, они довольно медленны по сравнению с FPU, встроенным в процессор. Выполнение одного последовательного вычисления с помощью графического процессора - большая потеря. ЦП должен заполнить конвейер до ГП, дождаться, пока медленный ГП выполнит эту единственную задачу, а затем выгрузить результат. В то время как графические процессоры абсолютно фантастичны, когда проблема поддается массовому распараллеливанию, они абсолютно ужасны, когда дело доходит до выполнения последовательных задач.
Дэвид Хаммен,
1
В исходном вопросе: «Поскольку этот код выполняется много раз, производительность вызывает беспокойство». Это на единицу больше, чем «много». Оператор может отправлять вычисления в многопоточном режиме.
user3791372 04
0

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

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

Фред Митчелл
источник
0

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

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

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

Удачи!

математика
источник