web-dev-qa-db-ja.com

Intel SandybridgeファミリーのCPUにおけるパイプラインプログラムの最適化

私はこの任務を完了しようとしている1週間の間私の頭脳を悩ませていました、そして、私はここの誰かが正しい道に向かって私を導くことができることを望みます。講師の指示から始めましょう。

あなたの課題は、素数プログラムを最適化することであった最初のラボ課題の反対です。この課題のあなたの目的はプログラムを悲観的にすること、すなわちそれをより遅く実行させることです。どちらもCPU集中型のプログラムです。彼らは私たちの研究室のPCで動作するように数秒かかります。アルゴリズムを変更してはいけません。

プログラムを最適化しないようにするには、Intel i7パイプラインがどのように動作するかについての知識を活用してください。 WAR、RAW、およびその他の危険を招くように命令パスを並べ替える方法を想像してください。キャッシュの有効性を最小限に抑える方法を考えてください。悪魔的に無能です。

課題は、ホイートストンまたはモンテカルロプログラムの選択を与えました。キャッシュ有効性のコメントは主にWhetstoneにのみ適用可能ですが、私はモンテカルロシミュレーションプログラムを選択しました。

// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm>    // Needed for the "max" function
#include <cmath>
#include <iostream>

// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in 
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
  double x = 0.0;
  double y = 0.0;
  double euclid_sq = 0.0;

  // Continue generating two uniform random variables
  // until the square of their "euclidean distance" 
  // is less than unity
  do {
    x = 2.0 * Rand() / static_cast<double>(Rand_MAX)-1;
    y = 2.0 * Rand() / static_cast<double>(Rand_MAX)-1;
    euclid_sq = x*x + y*y;
  } while (euclid_sq >= 1.0);

  return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}

// Pricing a European Vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(S_cur - K, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

// Pricing a European Vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(K - S_cur, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

int main(int argc, char **argv) {
  // First we create the parameter list                                                                               
  int num_sims = 10000000;   // Number of simulated asset paths                                                       
  double S = 100.0;  // Option price                                                                                  
  double K = 100.0;  // Strike price                                                                                  
  double r = 0.05;   // Risk-free rate (5%)                                                                           
  double v = 0.2;    // Volatility of the underlying (20%)                                                            
  double T = 1.0;    // One year until expiry                                                                         

  // Then we calculate the call/put values via Monte Carlo                                                                          
  double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
  double put = monte_carlo_put_price(num_sims, S, K, r, v, T);

  // Finally we output the parameters and prices                                                                      
  std::cout << "Number of Paths: " << num_sims << std::endl;
  std::cout << "Underlying:      " << S << std::endl;
  std::cout << "Strike:          " << K << std::endl;
  std::cout << "Risk-Free Rate:  " << r << std::endl;
  std::cout << "Volatility:      " << v << std::endl;
  std::cout << "Maturity:        " << T << std::endl;

  std::cout << "Call Price:      " << call << std::endl;
  std::cout << "Put Price:       " << put << std::endl;

  return 0;
}

私が行った変更により、コードの実行時間が1秒増加したように見えますが、コードを追加せずにパイプラインを停止させるために何を変更できるかについては完全にはわかりません。正しい方向へのポイントは素晴らしいだろう、私は任意の応答をいただければ幸いです。


更新: この課題を与えた教授は詳細を投稿しました

ハイライトは以下のとおりです。

  • コミュニティカレッジの2学期アーキテクチャクラスです(HennessyとPattersonの教科書を使用)。
  • ラボコンピュータにはHaswell CPUがあります
  • 生徒たちはCPUID命令とキャッシュサイズの決定方法、そして組み込み関数とCLFLUSH命令に触れました。
  • 任意のコンパイラオプションが許可されているので、インラインasmも同様です。
  • あなた自身の平方根アルゴリズムを書くことは青白いの外にあるとして発表されました

Cowmoogunのメタスレッドに対するコメントは、 コンパイラの最適化がこれに含まれる可能性があることは明らかではなかったこと、および-O0 を想定していたことを示しています。

したがって、課題の目的は、既存の作業を命令レベルの並列処理やそのようなことを減らすように並べ替えることです。しかし、人々がより深く掘り下げてより多くを学んだことは悪いことではありません。


これはコンピュータアーキテクチャの問題であり、C++を一般的に遅くする方法に関する問題ではないことに注意してください。

307
Cowmoogun

可能な限りパフォーマンスを低下させるためにできることはいくつかあります。

  • i386アーキテクチャ用のコードをコンパイルします。これはSSEおよびそれ以降の命令の使用を妨げ、x87 FPUの使用を強制します。

  • 至る所でstd::atomic変数を使用してください。これは、コンパイラがいたるところにメモリバリアを挿入することを余儀なくされているため、非常に高価になります。そしてこれは無能な人が「スレッドの安全性を確保する」ためにもっともらしいことをするかもしれないことです。

  • プリフェッチャが予測できる最悪の方法(列メジャーと行メジャー)でメモリにアクセスするようにしてください。

  • あなたの変数をさらに高価にするために、それらに '自動記憶期間'(スタック割り当て)を持たせるのではなくnewを使ってそれらを割り当てることによってあなたはそれらすべてが '動的記憶期間'(ヒープ割り当て)を持つことを確かめます。

  • あなたが割り当てるすべてのメモリが非常に奇妙に整列されていることを確認し、巨大なページを割り当てることを絶対に避けてください。

  • 何をしても、コンパイラオプティマイザを有効にしてコードをビルドしないでください。そして、できる限り表現力豊かなデバッグシンボルを有効にするようにしてください( run を遅くすることはありませんが、余分なディスク容量が無駄になります)。

注:この回答は基本的に@Peter Cordesがすでに彼の非常に良い回答に取り入れているという私のコメントを要約したものです。もしあなたが予備のものしか持っていないなら、彼があなたの支持を得ていることを示唆しなさい:)

31
Jesper Juhl

計算にはlong doubleを使うことができます。 x86では、それは80ビットフォーマットであるべきです。従来のx87 FPUのみがこれをサポートしています。

X87 FPUのいくつかの欠点:

  1. SIMDの欠如は、より多くの指示が必要な場合があります。
  2. スタックベースで、スーパースカラアーキテクチャとパイプラインアーキテクチャでは問題があります。
  3. 別々の非常に小さなレジスタセットは、他のレジスタからのより多くの変換とより多くのメモリ操作を必要とするかもしれません。
  4. Core i7では、SSE用に3つのポートがあり、x87用に2つしかないため、プロセッサはより少ない並列命令を実行できます。
10
Michas

回答が遅れていますが、リンクリストとTLBを悪用しているとは思えません。

Mmapを使用して自分のノードを割り当て、ほとんどの場合アドレスのMSBを使用するようにします。これは長いTLBルックアップチェーンをもたらすべきであり、ページは12ビットであり、変換のために52ビットを残すか、または毎回トラバースしなければならない約5レベルである。ちょっと運が良ければ、5レベルのルックアップに加えて1回のメモリアクセスでノードに到達するために毎回メモリにアクセスする必要があります。おそらくトップレベルはどこかにキャッシュされているはずです。最悪の境界をまたぐようにノードを配置して、次のポインタを読むとさらに3-4の変換ルックアップが行われるようにします。大量の変換ルックアップのために、これもキャッシュを完全に破壊する可能性があります。また、仮想テーブルのサイズによって、ほとんどのユーザーデータが長時間ディスクにページングされる可能性があります。

単一のリンクリストから読み取るときは、毎回リストの先頭から読み取ることを忘れないでください。

3
Surt