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)は良い提案でした。

これはCPUで実行されます。近い将来、これはGPUでよりよく実行される可能性がありますが、現時点ではそのオプションは使用できません。


32
(コンパイラーがそれ自体を最適化しない限り)最初に頭に浮かぶのは、これらすべてpow(l1 * l2 * l3, -0.1e1 / 0.3e1)を変数に置き換えることです...ただし、コードが高速か低速かを確認するには、コードをベンチマークする必要があります。
SingerOfTheFall

6
また、コードを読みやすくするためにフォーマットします-改善の可能性を識別するのに役立ちます。
Ed Heal

26
なぜすべての反対投票と投票が終了するのですか?数値プログラミングや科学プログラミングが苦手な方は、他の質問をご覧ください。これは、このサイトに適した良い質問です。scicompサイトはまだベータ版です。移行は良いオプションではありません。コードレビューサイトは十分な目を見張っていません。科学計算でOPが頻繁に発生すること:シンボリック数学プログラムで問題を作成し、プログラムにコードを生成するように依頼します。生成されたコードは非常に混乱しているため、結果に触れないでください。
David Hammen、2015年

6
@DavidHammen コードレビューサイトでは十分な目が得られません -鶏と卵の問題のように聞こえ、そのようなをCRが獲得するのに役立たない考え方です。scicompベータサイトはベータ版であるため、これを断るという考えにも同じことが当てはまります。誰もがそのように考えた場合、成長する唯一のサイトはStack Overflowになります。
Mathieu Guindon、2015年

13
この質問はここ
NathanOliver '10 / 10/3

回答:


88

概要を編集

  • 私の最初の答えは、コードに多くの複製された計算が含まれていて、その能力の多くが1/3の要素を含んでいることを指摘しただけです。たとえばpow(x, 0.1e1/0.3e1)、はと同じcbrt(x)です。
  • 私の2番目の編集は間違っていたので、3番目の編集はこの間違いを推定したものです。これが、人々が文字「M」で始まる象徴的な数学プログラムからの神託のような結果を変更することを恐れさせるものです。私はそれらの編集を打ち消し(つまり、ストライク)、この回答の現在のリビジョンの一番下にプッシュしました。ただし、削除しませんでした。私は人間です。私たちは間違いを犯しやすい。
  • 私の第四編集は正しく、問題の複雑な式を表していることを非常にコンパクトな表現開発のIFパラメータl1l2およびl3場合は正の実数であり、a非ゼロの実数であるが。(これらの係数の具体的な性質については、OPからまだ連絡がありません。問題の性質を考えると、これらは合理的な仮定です。)
  • この編集は、これらの式を単純化する方法の一般的な問題に答えようとします。

まず最初に

Mapleを使用してC ++コードを生成し、間違いを回避しています。

MapleとMathematicaは時々明白を逃します。さらに重要なことに、MapleとMathematicaのユーザーは時々間違いを犯します。「時々」、または「ほとんど常に」の代わりに「時々」を置き換えることは、「時々、マークに近い」でしょう。

問題のパラメータについて伝えることで、Mapleが式を簡略化するのを助けることができます。手近な例では、私はそれを疑うl1l2と、l3正の実数であり、それがaゼロ以外の実数です。その場合は、それを伝えてください。これらのシンボリック数学プログラムは、通常、手元にある数量が複雑であると想定しています。ドメインを制限すると、プログラムは複素数では無効な仮定を行うことができます。


シンボリック数学プログラムからこれらの大きな混乱を単純化する方法(この編集)

シンボリック数学プログラムは通常、さまざまなパラメーターに関する情報を提供する機能を提供します。特に、問題に除算や指数が含まれる場合は、その能力を使用してください。手近な例では、ということを伝えることで、その式の簡素化メープルを助けたかもしれないl1l2と、l3正の実数であり、それがaゼロ以外の実数です。その場合は、それを伝えてください。これらのシンボリック数学プログラムは、通常、手元にある数量が複雑であると想定しています。ドメインを制限すると、プログラムはa x b x =(ab)xなどの仮定を行うことができます。これはab正の実数であり、かつ実数である場合に限られxます。複素数では無効です。

最終的に、これらのシンボリック数学プログラムはアルゴリズムに従います。それを手伝ってください。コードを生成する前に、拡張、収集、および簡略化を試してみてください。このケースでは、それらの要因が関与する観点から収集している可能性muの要因が関与するものをK。表現を「最も単純な形式」に縮小することは、まだ芸術の一部です。

生成されたコードの醜い混乱を受け取ったら、それをあなたが触れてはならない真実として受け入れないでください。自分で簡単にしてみてください。シンボリック数学プログラムがコードを生成する前に持っていたものを見てください。私があなたの表現をもっと単純ではるかに速いものにどのように減らしたか、そしてウォルターの答えが私にさらにいくつかのステップを取った方法を見てください。魔法のレシピはありません。魔法のレシピがあれば、メープルはそれを適用し、ウォルターが答えたでしょう。


特定の質問について

あなたはその計算で多くの加算と減算を行っています。互いにほとんど打ち消し合う条件がある場合、深刻な問題に陥ることがあります。1つの用語が他の用語よりも優位である場合、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、上記は次のようになります。l2l3

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、上記はゼロになります。このゼロの計算は何度も繰り返されます。l2l3

2回目の更新

私が数学の正しいことをした場合(私が数学の正しいことをしたという保証はありません)、質問の厄介な表現は次のようになります。

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);

上記のことを前提としl1l2、およびl3正の実数です。


2
まあ、CSEの除去、緩やかなセマンティクス(およびOPのコメントに示されているOP)とは関係なく機能するはずです。もちろん、それが重要な場合(測定される場合)は、検査する必要があります(生成されたアセンブリ)。用語の支配、式の簡略化の欠落、より特殊化された関数、キャンセルの危険性についてのあなたのポイントは非常に良いものです。
Deduplicator

3
@Deduplicator-浮動小数点ではありません。安全でない数学最適化を有効にしない限り(たとえば、-ffast-mathgccまたはclangで指定することにより)、コンパイラはpow(x,-1.0/3.0)に等しいことを信頼できませんx*pow(x,-4.0/3.0)。前者はアンダーフローし、後者はアンダーフローする可能性があります。浮動小数点標準に準拠するために、コンパイラーはその計算をゼロに最適化してはなりません。
David Hammen、2015年

まあ、それらは私が意図したものよりもかなり野心的です。
Deduplicator

1
@Deduplicator:別の答えについてコメントしたように-fno-math-errno、g ++からCSEへの同一のpow呼び出しが必要です。(おそらく、捕虜がerrnoを設定する必要がないことを証明できる場合を除きますか?)
Peter Cordes

1
@レフティ-ウォルターの答えをよく見てください。それはかなり速いです。これらすべての回答には潜在的な問題があります。それは数値のキャンセルです。あなたと仮定するとN1N2、およびN3非負ある、のいずれかの2*N_i-(N_j+N_k)マイナスとなり、1が正となり、他方が、間にどこかになります。これにより、数値のキャンセル問題が簡単に発生する可能性があります。
David Hammen、2015年

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指数を持ついくつかを持っていますaaが整数であり、コンパイラー時に既知である場合、それらを置き換えて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-errnopowループの外への同一の呼び出しを引き上げることができるのはg ++ だけです。ほとんどのコードにとって、これは-ffast-mathの最も「危険な」部分です。
Peter Cordes、2015年

1
@PeterCordesこれらは興味深い結果です!またpow 、非常に遅いという問題もありdlsym、コメントで述べたハックを使用して、実際には少し精度を落としてパフォーマンスを大幅に向上させることができました。
tpg2114

GCCは、powが純粋な関数であることを理解しませんか?それはおそらく組み込みの知識でしょう。
usr

6
@usr:それはちょうどポイントだと思います。 標準によると、これは純粋な関数でpowはありませんerrno。これは、状況によって設定されることが想定されているためです。などのフラグを設定すると、フラグが設定さ-fno-math-errnoなくなりますerrno(したがって、標準に違反します)が、純粋な関数であり、そのように最適化できます。
Nate Eldredge、2015年

20

うわぁ、なんてひどい表現なんだ。Mapleで式を作成することは、実際には次善の選択でした。結果は単に判読できません。

  1. 話す変数名を選択しました(l1、l2、l3ではなく、高さ、幅、奥行きなど、意味する場合)。そうすれば、自分のコードを理解しやすくなります。
  2. 複数回使用するサブタームを事前に計算し、その結果を発話する名前の変数に格納します。
  3. あなたは、式が非常に何度も評価されることを述べます。おそらく、最も内側のループで変化するパラメーターはごくわずかです。そのループの前にすべての不変部分項を計算します。すべての不変条件がループの外側になるまで、2番目の内部ループについても同様に繰り返します。

理論的には、コンパイラーはこれらすべてを実行できるはずですが、それができない場合もあります。たとえば、ループのネストが異なるコンパイル単位の複数の関数にまたがっている場合などです。とにかく、読みやすく、理解しやすく、保守しやすいコードを提供します。


8
「コンパイラーはそれを行うべきですが、そうでない場合もあります」がここで重要です。もちろん、可読性に加えて。
ハビエル

3
コンパイラが何かをする必要がない場合は、ほとんどの場合それが間違っていると仮定します。
edmz

4
話す変数名を再度選択する -数学をしているとき、その良いルールが適用されないことがよくあります。科学ジャーナルでアルゴリズムを実装することになっているコードを見ると、コード内の記号がジャーナルで使用されているものとまったく同じであることがわかります。通常、これは非常に短い名前を意味し、添え字が付いている可能性があります。
David

8
「結果は単に判読できない」-なぜそれが問題なのですか?レクサーまたはパーサージェネレーターからの高水準言語出力が(人間によって)「読めない」ことを気にする必要はありません。ここで重要なのは、コードジェネレーター(Maple)入力が読み取り可能かつチェック可能であることです。事ではないあなたはそれがエラーフリーである確信になりたい場合は行うには、手動で生成されたコードを編集です。
alephzero

3
@DavidHammen:まあ、その場合には、1文字のものがあり、「話す名前が」。例えば、2次元のデカルトでジオメトリを行う際の座標系、xそしてyありません、彼らは全体あり、無意味な単一文字の変数の単語正確な定義と同様、広く理解される意味を持ちます。
イェルクWミッターク

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()場合は、呼び出しと呼び出しの1つを回避できます。これを段階的にもう一度やってみましょう。

// 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。次に、への呼び出しは2つだけ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==l2l1==l3、またはl2==l3、一方または両方のコールにはpow排除することができます。したがって、そのような可能性が現実的に存在する場合は、これらを特別なケースと見なすことは価値があります。


@gnat私はあなたの編集に感謝します(私はそれを自分で行うことを考えました)が、Davidの回答もこれにリンクするなら、より公正であるとわかりました。同様に、Davidの回答も編集してみませんか?
Walter

1
編集したのは、あなたが明示的に言及しているのを見たからです。私はデビッドの答えを読み返しましたが、そこにあなたの答えへの言及が見つかりませんでした。私が追加したものが作者の意図と一致することが100%明確でない編集は避けようとします
gnat

1
@ウォルター-私の答えはあなたのリンクになりました。
David Hammen、2015年

1
確かに私ではなかった。数日前にあなたの回答に賛成しました。また、私の回答に対してランダムなフライバイダウン投票を受け取りました。何かが時々起こるだけです。
David Hammen、2015年

1
あなたと私は、それぞれわずかな反対投票を受けました。質問に対するすべての反対票を見てください!現在のところ、質問には16の反対票が寄せられています。また、80件の賛成票を受け取り、それらすべての反対票を相殺しました。
David

12
  1. 「多数」はいくつですか?
  2. それはどのくらいかかりますか?
  3. DO ALLのパラメータは、この式の再計算の間で変更できますか?または、事前に計算された値をキャッシュできますか?
  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;

[追加]最後の3行の式にさらに取り組んで、この美しさを理解しました。

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」は数式用です)。
Vlad Feinstein、2015年

私が見たほとんどのF77コードはそのように見えました(たとえば、BLASおよびNR)。非常に嬉しいFortran 90-> 2008が存在します:)
カイルカノス

はい。式を翻訳する場合、FORmulaTRANslationよりも優れた方法は何ですか?
ブライアンドラモンド

1
あなたの「最適化」は間違った場所を攻撃します。コストのかかるビットはへの呼び出しであり、そのうちの6、3 std::pow()倍必要です。言い換えると、コードは可能な速度より3倍遅くなります。
Walter

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)ます。

これは現在の問題に直接適用できませんが、整数のべき乗を持つ高次多項式がある場合に役立ちます。操作の順序は、そのために重要であるため(一般Iに実際ホーナーフォームがあるため、このために役立ちますだと思うが、あなたは、数値的安定性やオーバーフローの問題に注意する必要があるかもしれませんx^20し、x通常は離れて何桁です)。

また、実用的なヒントとして、まだ行っていない場合は、まずmapleの式を簡略化してみてください。おそらく、一般的な部分式の除去のほとんどを実行することができます。特にそのプログラムのコードジェネレーターにどの程度影響するかはわかりませんが、Mathematicaではコードを生成する前にFullSimplifyを実行すると大きな違いが生じる可能性があることを知っています。


ホーナー形式は、多項式をコーディングするためのかなり標準的なものであり、これは問題とはまったく関係がありません。
ウォルター、

1
これは彼の例を考えると当てはまるかもしれませんが、彼が「このタイプの方程式」と言ったことに気付くでしょう。投稿者のシステムに多項式が含まれている場合は、答えが役立つと思いました。特に気づいたのは、MathematicaやMapleなどのCASプログラムのコードジェネレーターは、特に要求しない限り、Horner形式を提供しない傾向があることです。デフォルトでは、通常、多項式を人間として書く方法になります。
neocpp 2015年

3

繰り返し操作がたくさん行われているようです。

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

それらを事前に計算して、pow高価な関数を繰り返し呼び出さないようにすることができます。

事前計算することもできます

l1 * l2 * l3

その用語を繰り返し使用するので。


6
オプティマイザーはすでにこれを行っているに違いありませんが、少なくともコードが読みやすくなります。
Karoly Horvath、2015年

私はこれをしました、しかしそれは物事を全くスピードアップしませんでした。これは、コンパイラの最適化がすでに処理していたためだと考えました。

l1 * l2 * l3を格納すると処理速度は向上しますが、コンパイラの最適化で理由が

なぜなら、コンパイラーは時々、いくつかの最適化を実行できないか、他のオプションと競合しているのを見つけることができないからです。
ハビエル

1
実際、-ffast-mathが有効になっていない限り、コンパイラーはこれらの最適化を行わないでください。@ tpg2114のコメントに記載されているように、その最適化は非常に不安定な結果をもたらす可能性があります。
David Hammen

0

Nvidia CUDAグラフィックカードを使用している場合は、計算をグラフィックカードにオフロードすることを検討できます。これは、計算が複雑な計算に適しています。

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

そうでない場合は、計算用に複数のスレッドを検討することをお勧めします。


10
この答えは、当面の質問に直角です。GPUには多くのプロセッサが搭載されていますが、CPUに埋め込まれたFPUに比べるとかなり低速です。GPUで単一のシリアル計算を実行することは大きな損失です。CPUは、GPUへのパイプラインを満たし、低速のGPUがその単一のタスクを実行するのを待ってから、結果をアンロードする必要があります。GPUは、当面の問題が大規模な並列化が可能な場合は非常に素晴らしいですが、シリアルタスクの実行に関しては非常にひどいです。
David Hammen、2015年

1
元の質問:「このコードは何度も実行されるため、パフォーマンスが問題になります。」それは「多くの」以上のものです。opはスレッド化された方法で計算を送信できます。
user3791372

0

万が一、計算を象徴的に提供していただけませんか。ベクトル演算がある場合、場合によっては並列で演算を実行できるblasまたはlapackを使用して調査することをお勧めします。

numpyやscipyでpythonを使用できる可能性があると(トピックから外れる危険性はありますか?)考えられます。可能な範囲で、計算が読みやすくなる場合があります。


0

高レベルの最適化について明示的に尋ねたように、さまざまなC ++コンパイラを試す価値があるかもしれません。現在、コンパイラーは非常に複雑な最適化獣であり、CPUベンダーは非常に強力で特定の最適化を実装する可能性があります。ただし、無料ではないものもあります(ただし、無料の学術プログラムがある場合もあります)。

  • GNUコンパイラコレクションは無料で柔軟で、多くのアーキテクチャで利用できます
  • Intelコンパイラは非常に高速で非常に高価であり、AMDアーキテクチャに対しても良い結果をもたらす可能性があります(学術プログラムがあると思います)
  • Clangコンパイラは高速で無料であり、GCCと同様の結果を生成する可能性があります(一部の人々は高速で優れていると言いますが、これはアプリケーションごとに異なる可能性があるため、独自のエクスペリエンスを作成することをお勧めします)
  • PGI(Portland Group)は、Intelコンパイラとして無料ではありません。
  • PathScaleコンパイラーはAMDアーキテクチャーで良い結果をもたらすかもしれません

コードスニペットの実行速度が2倍異なるのは、コンパイラーを変更するだけで(もちろん、完全に最適化されている場合のみ)見たことがあります。ただし、出力のIDの確認に注意してください。積極的な最適化を行うと、出力が異なる可能性がありますが、これは絶対に避けたいものです。

幸運を!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.