ブラックショールズ方程式を使用したヨーロピアン・オプションの価格計算をスピードアップする。
oneMKL ベクトルマス関数を使用して計算をスピードアップします。
ブラックショールズ・モデルは、市場の行動を連立確率微分方程式で表します [Black73]。この市場で発行されるヨーロピアン・オプション (コールおよびプット) は、ブラックショールズ方程式に従って価格付けされます。
説明:
Vcall /Vput はコール/プットオプションの現在の値、S0 は現在の株価、X は権利行使価格、r はリスク中立レート、σ はボラティリティー、T は満期日、CDF は標準正規分布の累積分布関数です。
累積正規分布関数との関係が単純な誤差関数 ERF を使用することもできます。
ソースコード: サンプル (https://www.intel.com/content/dam/develop/external/us/en/documents/mkl-cookbook-samples-120115.zip (英語)) の black-scholes フォルダーを参照してください。GitHub の black_scholes (英語) も参照してください。
このコードは、コールおよびプットオプションの価格計算にクローズド形式のソリューションを実装します。
void BlackScholesFormula( int nopt,
tfloat r, tfloat sig, const tfloat s0[], const tfloat x[],
const tfloat t[], tfloat vcall[], tfloat vput[] ) {
tfloat d1, d2;
int i;
for ( i=0; i<nopt; i++ ) {
d1 = ( LOG(s0[i]/x[i]) + (r + HALF*sig*sig)*t[i] ) / ( sig*SQRT(t[i]) );
d2 = ( LOG(s0[i]/x[i]) + (r - HALF*sig*sig)*t[i] ) / ( sig*SQRT(t[i]) );
vcall[i] = s0[i]*CDFNORM(d1) - EXP(-r*t[i])*x[i]*CDFNORM(d2);
vput[i] = EXP(-r*t[i])*x[i]*CDFNORM(-d2) - s0[i]*CDFNORM(-d1);
}
}
オプションの数は nopt パラメーターで指定します。tfloat の型は、使用する精度に応じて float または double のいずれかです。同様に、LOG、EXP、SQRT、CDFNORM を数学関数の単精度または倍精度バージョンにマップします。定数 HALF は 0.5f (単精度) または 0.5 (倍精度) のいずれかです。
nopt に加えて、アルゴリズムの入力パラメーターは、s0 (現在の株価)、r (リスク中立レート)、sig (ボラティリティー)、t (満期日)、x (権利行使価格) です。結果は vcall および vput (コールオプションとプットオプションの現在の値) で返されます。
r および sig は価格付けを行うすべてのオプションの定数であり、ほかのパラメーターは浮動小数点値の配列であると仮定しています。vcall および vput パラメーターは出力配列です。
超越関数は、ブラックショールズ方程式ベンチマークの中心となるものです。しかし、各オプションの値は 5 つのパラメーターに依存しており、計算が速くなるとメモリー効率がより影響するようになります。そのため、配列パラメーターの数は計算による制限からメモリー帯域幅による制限に計算を変更する重要な要因です。さらに、価格計算のオプションが膨大な場合、メモリーサイズの制約を考慮すべきです。
インテル® C++ コンパイラーは、ブラックショールズ方程式に関してインテル® アーキテクチャーの SIMD およびマルチコア性能を引き出す、ベクトル化と並列化機能を提供します。最適化されベクトル化された数学関数は、SVML (Short Vector Mathematical Library) ランタイム・ライブラリーで利用可能です。
oneMKL ベクトルマス (VM) 関数は、式の計算をさらにスピードアップするために使用できる、高度にチューニングされた超越数学関数を提供します。
コードを最適化する機会は、共通の部分式のループ外への移動、CDFNORM 関数から ERF 関数 (通常はより高速) への置換、関係 ERF(-x) = -ERF(x) の活用、SQRT による除算から平方根の逆数 INVSQRT による乗算 (通常はより高速) への置換、などいくつかあります。
void BlackScholesFormula( int nopt,
tfloat r, tfloat sig, tfloat s0[], tfloat x[],
tfloat t[], tfloat vcall[], tfloat vput[] ) {
int i;
tfloat a, b, c, y, z, e;
tfloat d1, d2, w1, w2;
tfloat mr = -r;
tfloat sig_sig_two = sig * sig * TWO;
for ( i = 0; i < nopt; i++ ) {
a = LOG( s0[i] / x[i] );
b = t[i] * mr;
z = t[i] * sig_sig_two;
c = QUARTER * z;
e = EXP ( b );
y = INVSQRT( z );
w1 = ( a - b + c ) * y;
w2 = ( a - b - c ) * y;
d1 = ERF( w1 );
d2 = ERF( w2 );
d1 = HALF + HALF*d1;
d2 = HALF + HALF*d2;
vcall[i] = s0[i]*d1 - x[i]*e*d2;
vput[i] = vcall[i] - s0[i] + x[i]*e;
}
}
このコードで、INVSQRT(x) は精度に応じて 1.0/sqrt(x) または 1.0f/sqrtf(x) のいずれかです。TWO および QUARTER はそれぞれ、浮動小数点定数 2 と 0.25 です。
いくつかの最適化はインテル® C/C++ コンパイラーを使用した効率的なコードの生成に役立ちます。このセクションで推奨しているコンパイラーのプラグマおよびスイッチに関する詳細は、『インテル® C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』を参照してください。
#pragma simd は、ループをベクトル化するようにコンパイラーに伝えます。また、#pragma vector aligned は、配列がアライメントされていて (メモリー割り当て段階でベクトルを適切にアライメントする必要があります) アライメント済みロード命令およびストア命令が安全であることをコンパイラーに伝えます。SVML で利用可能な、効率的なベクトル化を行うことにより、スカラーコードの数倍のスピードアップを達成できます。
…
#pragma simd
#pragma vector aligned
for ( i = 0; i < nopt; i++ )
…
これらの変更により、すべての利用可能な CPU コアを活用できます。最も簡単な方法は、コンパイラーがコードを自動的に並列化するように、コンパイル行に -autopar スイッチを追加することです。別のオプションは、標準 OpenMP* ディレクティブを使用することです。
…
#pragma omp parallel for
for ( i = 0; i < nopt; i++ )
…
-fp-model fast、-no-prec-div、-ftz、-fimf-precision、-fimf-max-error、-fimf-domain-exclusion などのインテル® C++ コンパイラー・オプションを使用して数学関数の精度を緩和できる場合、さらにパフォーマンスを向上することが可能です。
Linux* 固有のコンパイラー・スイッチも用意されています。詳細は、『インテル® C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』を参照してください。
並列度が非常に高い場合、数学関数の計算時間がメモリー帯域幅よりも低くなり、ループのパフォーマンスを制限することがあります。この制限は、並列化による直線的なスピードアップの障害となります。その場合は、メモリー帯域幅の処理に優れた非テンポラルなロード/ストア命令を使用します。
…
#pragma vector nontemporal
for ( i = 0; i < nopt; i++ )
…
oneMKL VM コンポーネントは、さらなるパフォーマンス向上に役立つ高度にチューニングされた超越数学関数を提供します。しかし、これらの関数を使用するには、VM API のベクトル機能に対応するコードのリファクタリングが必要になります。次のコードサンプルでは、自明でない数学関数は VM の関数を、残りの基本的な演算はコンパイラーの関数を利用しています。
ベクトルマス計算の中間結果を保持するため、一時バッファーが関数のスタックに割り当てられます。バッファーを適用可能な最大の SIMD レジスターサイズでアライメントすることが重要です。バッファーサイズは、VM 関数が (ベクトル関数のスタートアップにかかるコストを補い) 最適なパフォーマンスを達成でき、データがキャッシュに収まるように選択されます。バッファーサイズは実験して決定できます。推奨する開始サイズは NBUF=1024 です。
void BlackScholesFormula_MKL( int nopt,
tfloat r, tfloat sig, tfloat * s0, tfloat * x,
tfloat * t, tfloat * vcall, tfloat * vput ) {
int i;
tfloat mr = -r;
tfloat sig_sig_two = sig * sig * TWO;
#pragma omp parallel for \
shared(s0, x, t, vcall, vput, mr, sig_sig_two, nopt) \
default(none)
for ( i = 0; i < nopt; i+= NBUF ) {
int j;
tfloat *a, *b, *c, *y, *z, *e;
tfloat *d1, *d2, *w1, *w2;
__declspec(align(ALIGN_FACTOR)) tfloat Buffer[NBUF*4];
// nopt がNBUF の倍数でなかった場合
// ループの最後の反復のベクトル長を計算
#define MY_MIN(x, y) ((x) < (y)) ? (x) : (y)
int nbuf = MY_MIN(NBUF, nopt - i);
a = Buffer + NBUF*0; w1 = a; d1 = w1;
c = Buffer + NBUF*1; w2 = c; d2 = w2;
b = Buffer + NBUF*2; e = b;
z = Buffer + NBUF*3; y = z;
// 各スレッドにVML の精度を設定
vmlSetMode( VML_ACC );
VDIV(nbuf, s0 + i, x + i, a);
VLOG(nbuf, a, a);
#pragma simd
for ( j = 0; j < nbuf; j++ ) {
b[j] = t[i + j] * mr;
a[j] = a[j] - b[j];
z[j] = t[i + j] * sig_sig_two;
c[j] = QUARTER * z[j];
}
VINVSQRT(nbuf, z, y);
VEXP(nbuf, b, e);
#pragma simd
for ( j = 0; j < nbuf; j++ ) {
tfloat aj = a[j];
tfloat cj = c[j];
w1[j] = ( aj + cj ) * y[j];
w2[j] = ( aj - cj ) * y[j];
}
VERF(nbuf, w1, d1);
VERF(nbuf, w2, d2);
#pragma simd
for ( j = 0; j < nbuf; j++ ) {
d1[j] = HALF + HALF*d1[j];
d2[j] = HALF + HALF*d2[j];
vcall[i+j] = s0[i+j]*d1[j] - x[i+j]*e[j]*d2[j];
vput[i+j] = vcall[i+j] - s0[i+j] + x[i+j]*e[j];
}
}
}
同等の精度では、計算により制限される問題 (データが L2 キャッシュに収まる) の場合、oneMKL VM はインテル® コンパイラーと SVML ベースのソリューションよりも 30-50% 優れたパフォーマンスを実現します。この場合、キャッシュ読み取り/書き込み操作のレイテンシーは計算によりマスクされます。問題サイズが大きくなりメモリー帯域幅により制限されるようになると、メモリー使用量の最適化はさらに重要となり、中間バッファーを利用するインテルの VM ソリューションは SVML を利用したバッファリングなしの 1 パス・ソリューションに対する優位性を失います。
タスク |
ルーチン |
---|---|
mode パラメーターに従って VM 関数に新しい精度モードを設定し、以前の VM モードを oldmode に格納する |
vmlSetMode |
ベクトル a とベクトル b の要素単位の除算を実行する |
vsdiv/vddiv |
ベクトル要素の自然対数を計算する |
vsln/vdln |
ベクトル要素の逆平方根を計算する |
vsinvsqrt/vdinvsqrt |
ベクトル要素の指数を計算する |
vsexp/vdexp |
ベクトル要素の誤差関数の値を計算する |
vserf/vderf |