2011-02-07 4 views
4

実際には、そのLennard Jones潜在性の派生物です。その理由は、私が分子動力学プログラムを書いていることであり、最も積極的なコンパイラオプション(gcc * * -O3)でさえ、少なくとも80%の時間が次の関数に費やされているからです。lennard jonesの潜在的な機能を最適化する良い方法はありますか?

 
double ljd(double r) /* Derivative of Lennard Jones Potential for Argon with 
         respect to distance (r) */ 
{ 
    double temp; 
    temp = Si/r; 
    temp = temp*temp; 
    temp = temp*temp*temp; 
    return ((24*Ep/r)*(temp-(2 * pow(temp,2)))); 
}

このコードは、私のメインファイルにインポートするファイル "functs.h"からのものです。私はこのように一時変数を使うと機能が速くなると思っていましたが、作成するのは無駄です。私は静的を使うべきですか?また、コードはopenmpを使って並列に書かれているので、tempをグローバル変数として宣言することはできません。

変数EpとSiは(#defineを使用して)定義されています。私はC言語を約1ヶ月使っています。私は、GCCによって生成されたアセンブラコードを見てみましたが、私は完全に失われた\

+0

異なるプロセッサが異なる方法で浮動小数点を扱うので、使用しているプロセッサも指定する必要があります。おそらくあなたが使っているIntelのものでしょう。 – Skizz

+2

無駄な一時変数が導入されると、パフォーマンスが向上せず、読みやすさが損なわれるだけです - まともなコンパイラがそれ自身の値の繰り返し使用を検出する必要があります... – Christoph

+0

Intel Core 2 Duoプロセッサを使用しています – sn6uv

答えて

7

私はスタートのためにpow()にコールを取り除くになります。

double ljd(double r) /* Derivative of Lennard Jones Potential for Argon with 
         respect to distance (r) */ 
{ 
    double temp; 
    temp = Si/r; 
    temp = temp * temp; 
    temp = temp * temp * temp; 
    return ((24.0 * Ep/r) * (temp - (2.0 * temp * temp))); 
} 
+0

私は実際に 'pow'への呼び出しを取り除くだけでどれくらいのスピードアップが得られるかに興味があります。私はそれがかなり印象的でなければならないと思っていますが、私はパフォーマンス面を推測するのが好きではありません。 –

+0

pow()への呼び出しを取り除くと約20%の増加が見られました – sn6uv

+0

私はスピードアップが主に 'pow()'がエラーチェックをしなければならないことが原因だと思っています... – Christoph

1

まあ、私が言ってきたようにこれまでは、コンパイラは多くの理由で浮動小数点コードの最適化に失敗しました。だから、ここ(DevStudioの2005を使用してコンパイル)より高速である必要があり、インテルのアセンブリのバージョンがあります:

const double Si6 = /*whatever pow(Si,6) is*/; 
const double Si_value = /*whatever Si is*/; /* need _value as Si is a register name! */ 
const double Ep24 = /*whatever 24.Ep is*/; 

double ljd (double r) 
{ 
    double result; 
    __asm 
    { 
    fld qword ptr [r] 
    fld st(0) 
    fmul st(0),st(0) 
    fld st(0) 
    fmul st(0),st(0) 
    fmulp st(1),st(0) 
    fld qword ptr [Si6] 
    fdivrp st(1),st(0) 
    fld st(0) 
    fld1 
    fsub st(0),st(1) 
    fsubrp st(1),st(0) 
    fmulp st(1),st(0) 
    fld qword ptr [Ep24] 
    fmulp st(1),st(0) 
    fdivrp st(1),st(0) 
    fstp qword ptr [result] 
    } 

    return result; 
} 

このバージョンが掲載さにわずかに異なる結果を生成します。コンパイラはおそらく中間結果を元のコードのRAMに書き込んでいます。 (インテル)FPUは内部で80ビットで動作し、ダブルタイプは64ビットでしか動作しないため、精度が低下します。上記のアセンブラは、中間結果の精度を失うことはありません。すべて80bitsで実行されます。最終結果のみが64ビットに丸められます。 -ffast-mathが提供されている場合、私のアーキテクチャ(インテルのCentrino Duoプロセッサ、WindowsのXP上のMinGW-GCC 4.5.2)で

+0

私はgccのインラインアセンブラを使用していません。gccコンパイラで動作するバージョンを誰かが追加できれば大いに感謝します。 – Skizz

+0

これはとても役に立ちます。Skizzに感謝します。私は1つの質問がある。 Siの値は3.405E-10であり、rは最大約7E-10である。 Si^6(1行目)の使用は大きな精度上の問題を引き起こしますか? – sn6uv

+0

Hands up - the-phast-mathオプションはかなりクールです。明らかに、私は最後にそれを詳細に調べたので、コンパイラはちょっとしたことがあります。 – Skizz

3

pow()

static inline double ljd(double r) 
{ 
    return 24 * Ep/Si * (pow(Si/r, 7) - 2 * pow(Si/r, 13)); 
} 

を使用して、非最適化されたコードは、実際にあなたのバージョンよりも性能が優れています。 (EpSiのために、いくつかの任意の値を使用して)生成されたアセンブリは、次のようになります

:いくつかの思い出を私に思い出させる

fldl LC0 
fdivl 8(%ebp) 
fld  %st(0) 
fmul %st(1), %st 
fmul %st, %st(1) 
fld  %st(0) 
fmul %st(1), %st 
fmul %st(2), %st 
fxch %st(1) 
fmul %st(2), %st 
fmul %st(0), %st 
fmulp %st, %st(2) 
fxch %st(1) 
fadd %st(0), %st 
fsubrp %st, %st(1) 
fmull LC1 
+0

私はこれまで知らなかった。これはgcc 4.4.5(Linux)の私のバージョンで提供されていて、かなりスピードアップしているようです。私はまだ正しく測定しようとしていません。ありがとう。 – sn6uv

+0

私は簡単なテストをしました。私が最初に投稿して渡したバージョンを使用しているときは12秒です。gccに-phast-mathを渡すと最大9秒間スピードアップし、上記のバージョンを使用して-ffast-mathは8sの時間を返します。 – sn6uv

+0

@Agnus: -intrinsicであり、効率的に最適化できますが、エラー処理やC標準で規定されているその他の浮動小数点関連の保証のため、デフォルトでは使用できません。結果をあなたの参照実装と比較して、安全でない操作を使用してもプログラムの正確さに影響を与えないようにしてください... – Christoph

1

ああ、私は潜在的な年前にレナード・ジョーンズとMDやりました。

私のシナリオ(巨大なシステムではない)では、pow()をいくつかの乗算で置き換えるには十分でした。私はまた近隣の範囲を制限し、事実上約r ~ 3.5の可能性を切り捨て、後で標準的な熱力学的補正を適用した。

しかし、これだけでは十分でない場合は、rという密接に離れた値の関数を事前に計算し、単純に補間することをお勧めします(線形または二次的です)。

1

あなたのアプリケーションは、この機能を有益にベクトル化し、いくつかの独立した値を並列に計算できるように構造化されていますか?これにより、SSEなどのハードウェアベクトルユニットを利用できます。

1/rの値をrの代わりに保存する方が良いと思われます。


これは、明示的にSSE2命令を使用してこの機能を実装した例です。 ljd()は2つの値を一度に計算します。

static __m128d ljd(__m128d r) 
{ 
    static const __m128d two = { 2.0, 2.0 }; 
    static const __m128d si = { Si, Si }; 
    static const __m128d ep24 = { 24 * Ep, 24 * Ep }; 

    __m128d temp2, temp3; 
    __m128d temp = _mm_div_pd(si, r); 
    __m128d ep24_r = _mm_div_pd(ep24, r); 

    temp = _mm_mul_pd(temp, temp); 
    temp2 = _mm_mul_pd(temp, temp); 
    temp2 = _mm_mul_pd(temp2, temp); 
    temp3 = _mm_mul_pd(temp2, temp2); 
    temp3 = _mm_mul_pd(temp3, two); 

    return _mm_mul_pd(ep24_r, _mm_sub_pd(temp2, temp3)); 
} 

/* Requires `out` and `in` to be 16-byte aligned */ 
void ljd_array(double out[], const double in[], int n) 
{ 
    int i; 

    for (i = 0; i < n; i += 2) 
    { 
     _mm_store_pd(out + i, ljd(_mm_load_pd(in + i))); 
    } 
} 

しかし、GCCの最近のバージョンは、多くの場合、限り、あなたは右のアーキテクチャをターゲットしていると最適化が有効になってきたように、自動的にこのような機能をvectoriseすることができることに注意することが重要です。 32ビットx86をターゲットにしている場合は、-msse2 -O3でコンパイルし、入力と出力の配列が16バイトに整列するように調整してください。

タイプアトリビュートが__attribute__ ((aligned (16)))のgcc、およびposix_memalign()機能を使用するダイナミックアレイの場合、スタティックアレイと自動アレイのアラインメントを実現できます。

+0

この関数は、私のプログラム。それは次のように機能します:あらゆるタイムステップで、すべての原子に対して、それ自身と他のすべての原子との間の力を計算します。他の原子を見ると、このプロセスを加速するために複数のスレッドが使用されます。要するに、私が "機能をベクトル化"できるかどうかはわかりません。 – sn6uv

+1

@Angus:時間ステップtでの計算はすべて互いに独立しているので、できるだけ聞こえます。例えば。あなたは '_mm_mul_pd()'のようなSSE2組み込み関数を使って2つの 'r'値の力を同時に計算することができます。 – caf

1

ローカル変数は問題ありません。それは何の費用もかかりません。ほっといて。

他の人が言ったように、powコールを取り除く。単純に数値を二乗するよりも速くなることはできません。それはロットです。

つまり、機能がアクティブであるため、時間の80%以上は問題ではありません。最適化できるものがあればを意味します。それはそこにあるか、それが(powのような)何かにあるか、それをと呼ぶものです。

スタックサンプリングの方法であるrandom pausingを試してみると、80 +%のサンプルとその時間内の行に加え、そのサンプルを担当する呼び出し元が表示されます時間はなどのようになります。スタック上のすべてのコード行は、共同で時間を管理します。

時間の大部分を占めるものは何もないときは、何もないときですあなたが修正できるのはです。大きな時間がかかります。

関連する問題