乾物丨DolphinDBジャストインタイムコンパイル(JIT)詳細

DolphinDBは、豊富なコンピューティング機能と強力なマルチパラダイムプログラミング言語が組み込まれた高性能の分散時系列データベースです。DolphinDBスクリプトの実行効率を向上させるために、バージョン1.01以降、DolphinDBはジャストインタイムコンパイル(JIT)をサポートしています。

1JIT入門

ジャストインタイムコンパイル(英語:ジャストインタイムコンパイル、略称:JIT)は、ジャストインタイムコンパイルまたはリアルタイムコンパイルとも呼ばれ、プログラム操作の効率を向上させることができる動的コンパイルの形式です。

通常、プログラムを実行するには、コンパイル実行とインタープリター実行の2つの方法があります。コンパイルと実行はすべて、プログラムが実行される前にマシンコードに変換されます。これは、C / C ++で表されるより高い操作効率を特徴としています。インタプリタ実行とは、インタプリタがプログラムを文ごとに解釈して実行することで、柔軟性は高くなりますが、Pythonに代表されるように実行効率は低くなります。

ジャストインタイムコンパイルは、2つの利点を組み合わせたものです。実行時にコードをマシンコードに変換すると、静的にコンパイルされた言語と同様の実行効率を実現できます。Pythonのサードパーティ実装であるPyPyは、JITを介してインタープリターのパフォーマンスを大幅に向上させます。Java実装の大部分は、コード効率を向上させるためにJITに依存しています。

2DolphinDBにおけるJITの役割

DolphinDBのプログラミング言語が解釈され、実行されます。プログラムが実行されると、プログラムは最初に解析されて構文ツリーが生成され、次に再帰的に実行されます。ベクトル化が使用できない場合、通訳費用は比較的高くなります。これは、DolphinDBの最下層がC ++によって実装されており、スクリプト内の関数呼び出しがC ++内の複数の仮想関数呼び出しに変換されるためです。forループ、whileループ、if-elseなどのステートメントでは、関数を繰り返し呼び出すのに非常に時間がかかり、シナリオによってはリアルタイムの要件を満たすことができません。

DolphinDBのジャストインタイムコンパイル関数は、forループ、whileループ、およびif-elseステートメントの実行速度を大幅に向上させます。これは、ベクトル化された操作を使用できないが、高速などの非常に高速な要件があるシナリオに特に適しています。 -周波数係数の計算、リアルタイムストリーミングデータ処理など。

次の例では、do-whileループに必要な時間を比較して、JITを使用した場合と使用しない場合の1〜1,000,000の合計を100回計算します。

def sum_without_jit(v) {
  s = 0l
  i = 1
  n = size(v)
  do {
    s += v[i]
    i += 1
  } while(i <= n)
  return s
}

@jit
def sum_with_jit(v) {
  s = 0l
  i = 1
  n = size(v)
  do {
    s += v[i]
    i += 1
  } while(i <= n)
  return s
}

vec = 1..1000000

timer(100) sum_without_jit(vec)     // 120552.740 ms
timer(100) sum_with_jit(vec)        //    290.065 ms
timer(100) sum(vec)                 //     48.922 ms

JITを使用しない場合の消費時間はJITを使用する場合の415倍であり、組み込みの合計関数を使用する場合は約1/7かかります。組み込み関数は、NULL値をチェックする命令が多数あるため、JITよりも高速です。 JITによって生成されたコード。組み込みの合計関数が次の場合入力配列にNULL値がない場合、このステップは省略されます。

vec[100] = NULL
timer(100) sum(vec)        // 118.063 ms

NULL値を加算すると、組み込み和の速度はJITの約2.5倍になります。これは、組み込み和も手動で最適化されているためです。より複雑な計算が関数に含まれる場合、JITの速度はベクトル化された操作を超えます。これについては以下で説明します。

タスクがベクトル化計算を使用できる場合、JITは場合によっては使用されない場合がありますが、高周波因子生成などの実際のアプリケーションでは、ループ計算をベクトル化計算に変換する方法には一定のスキルが必要です。

Zhihuに、我々は次のように売買シグナルを計算するための式はDolphinDB、でベクトル化操作を使用する方法を示しました:

direction = (iif(signal>t1, 1h, iif(signal<t10, 0h, 00h)) - iif(signal<t2, 1h, iif(signal>t20, 0h, 00h))).ffill().nullFill(0h)

DolphinDBを初めて使用する場合iifは、上記のステートメントを作成するための関数を理解する必要があります。forループを使用して上記のステートメントを書き直す方が簡単です。

@jit
def calculate_with_jit(signal, n, t1, t10, t20, t2) {
  cur = 0
  idx = 0
  output = array(INT, n, n)
  for (s in signal) {
    if(s > t1) {           // (t1, inf)
      cur = 1
    } else if(s >= t10) {  // [t10, t1]
      if(cur == -1) cur = 0
    } else if(s > t20) {   // [t20, t10)
      cur = 0
    } else if(s >= t2) {   // [t2, t20]
      if(cur == 1) cur = 0
    } else {               // (-inf, t2)
      cur = -1
    }
    output[idx] = cur
    idx += 1
  }
  return output
}

@jitを削除して、JITを使用しないカスタム関数を取得しますcalculate_without_jit3つの方法にかかる時間を比較します。

n = 10000000
t1= 60
t10 = 50
t20 = 30
t2 = 20
signal = rand(100.0, n)

timer(100) (iif(signal >t1, 1h, iif(signal < t10, 0h, 00h)) - iif(signal <t2, 1h, iif(signal > t20, 0h, 00h))).ffill().nullFill(0h) // 41092.019 ms
timer(100) calculate_with_jit(calculate, signal, size(signal), t1, t10, t20, t2)       //    17075.127 ms
timer(100) calculate_without_jit(signal, size(signal), t1, t10, t20, t2)               //  1404406.413 ms

この例では、JITを使用したベクトル化された操作の速度は、JITを使用しない場合の2.4倍です。DolphinDBの組み込み関数はベクトル化操作で何度も呼び出され、多くの中間結果が生成されるため、ここでのJITの速度はベクトル化操作よりも高速です。

複数のメモリ割り当てと仮想関数呼び出しを伴うため、JITによって生成されるコードにはこれらの追加のオーバーヘッドはありません。

別の状況は、特定の計算でベクトル化を使用できないことです。たとえば、オプションのインプライドボラティリティを計算する場合、ニュートン法が必要であり、ベクトル化は使用できません。この場合、ある程度のリアルタイムパフォーマンスを満たす必要がある場合は、DolphinDBプラグインを使用するか、JITを使用するかを選択できます2つの違いは、プラグインはどのシナリオでも使用できることですが、プラグインはC ++で記述する必要があり、より複雑です。JITの記述は比較的簡単ですが、適用できるシナリオはより限定されています。JITの実行速度は、C ++プラグインを使用する速度に非常に近いです。

3DolphinDBでJITを使用する方法

3.1使用方法

DolphinDBは現在、ユーザー定義関数のJITのみをサポートしています。ユーザー定義関数の前の行に@jitのロゴを追加するだけです。

@jit
def myFunc(/* arguments */) {
  /* implementation */
}

ユーザーがこの関数を呼び出すと、DolphinDBは関数のコードをリアルタイムでマシンコードにコンパイルして実行します。

3.2サポートされている文

DolphinDBは現在、JITで次のステートメントをサポートしています。

  • 割り当てステートメント、例:
@jit
def func() {
  y = 1
}

次に、複数の割り当ては現在サポートされていないことに注意してください。

@jit
def func() {
  a, b = 1, 2
}
func()

上記のステートメントを実行すると、例外がスローされます。

  • 例:returnステートメント:
@jit
def func() {
  return 1
}
  • 次のようなif-elseステートメント:
@jit
def myAbs(x) {
  if(x > 0) return x
  else return -x
}
  • たとえば、do-whileステートメント:
@jit
def mySqrt(x) {
    diff = 0.0000001
    guess = 1.0
    guess = (x / guess + guess) / 2.0
    do {
        guess = (x / guess + guess) / 2.0
    } while(abs(guess * guess - x) >= diff)
    return guess
}
  • ステートメントの場合、例:
@jit
def mySum(vec) {
  s = 0
  for(i in vec) {
    s += i
  }
  return s
}

DolphinDBは、JITでの上記のステートメントの任意のネストをサポートします。

3.3サポートされている演算子と関数

DolphinDBは現在、JITで次の演算子をサポートしています:add(+)、sub(-)、multiply(*)、divide(/)、and(&&)、or(||)、bitand(&)、bitor(|)、 bitxor(^)、eq(==)、neq(!=)、ge(> =)、gt(>)、le(<=)、lt(<)、neg(-)、mod(%)、seq (..)、at([])、すべてのデータ型での上記の操作の実装は、非JIT実装と一致しています。

DolphinDBは現在、JITで、次の数学関数をサポートしています exp、  log、  sin、  asin、  cos、  acos、  tan、  atan、  abs、  ceil、  floor、  sqrt上記の数学関数がJITに現れると、

受信したパラメーターがスカラーの場合、glibcの対応する関数または最適化されたC実装関数が、最終的に生成されたマシンコードで呼び出されます。受信したパラメーターが配列の場合、DolphinDBが最終的に呼び出されます。

提供される数学関数。これの利点は、Cによって実装されたコードを直接呼び出すことによって関数の効率を改善し、不要な仮想関数呼び出しとメモリ割り当てを減らすことです。

DolphinDBは現在、組み込み関数JITで次のようにサポートしていますtake、  array、  size、  isValid、  randcdfNormal

array関数の最初のパラメーターは特定のデータ型を直接指定する必要があり、変数転送では指定できないことに注意してください。これは、JITのコンパイル時にすべての変数の型を知る必要がarrayあり、関数の結果の型を最初のパラメーターで指定するため、コンパイル時に値を知る必要があるためです。

3.4null値の処理

JITのすべての関数と演算子は、ネイティブ関数と演算子と同じ方法でnull値を処理します。つまり、各データ型は型の最小値を使用して型のnull値を表し、ユーザーは処理する必要がありません。特にnull値を使用します。

3.5JIT関数間の呼び出し

DolphinDBのJIT関数は、別のJIT関数を呼び出すことができます。例えば:

@jit
def myfunc1(x) {
  return sqrt(x) + exp(x)
}

@jit
def myfunc2(x) {
  return myfunc1(x)
}

myfunc2(1.5)

上記の例では、内部が最初myfunc1コンパイルされ、署名がdouble myfunc1(double)のネイティブ関数がmyfunc2生成されます。この関数myfunc1は、JITであるかどうかを判断して実行するのではなく、生成されたマシンコードで直接呼び出されます。最高の実行効率を達成するために、実行時に機能します

型推論はこの方法では実行できないため、JIT以外のユーザー定義関数をJIT関数内で呼び出すことはできないことに注意してください。型控除については以下で説明します。

3.6JITコンパイルコストとキャッシュメカニズム

DolphinDBのJIT最下層はLLVMに依存し実現し、各ユーザー定義関数はコンパイル時に互いに独立した独自のモジュールを生成します。コンパイルには、主に次の手順が含まれます。

  1. LLVM関連の変数と環境の初期化
  2. DolphinDBスクリプトの構文ツリーに基づいてLLVMIRを生成します
  3. LLVMを呼び出して2番目のステップで生成されたIRを最適化し、それをマシンコードにコンパイルします

上記のステップの最初のステップは通常5ミリ秒未満で、次の2つのステップにかかる時間は実際のスクリプトの複雑さに比例します。全体として、コンパイル時間は基本的に50ミリ秒未満です。

JIT関数とパラメータータイプの組み合わせの場合、DolphinDBは1回だけコンパイルされます。システムは、JIT関数のコンパイル結果をキャッシュします。システムは、ユーザーがJIT関数を呼び出すときに提供されるパラメーターのデータ型に従って対応する文字列を取得し、ハッシュテーブルでこの文字列に対応するコンパイル結果を探し、存在する場合は直接呼び出します。存在しない場合はコンパイルを開始し、コンパイル結果をこのハッシュテーブルに保存して実行します。

繰り返し実行する必要のあるタスクや、実行時間が時間のかかるコンパイルをはるかに超えるタスクの場合、JITは実行速度を大幅に向上させます。

3.7制限

現在、DolphinDBで適用可能なJITのシナリオはまだ比較的限られています。

  1. JITはユーザー定義関数のみをサポートします。
  2. スカラー型と配列型のパラメーターのみが受け入れられます。table、dict、pair、string、symbolなどの他の型は現在サポートされていません。
  3. サブアレイはパラメーターとして受け入れられません。

4タイプの派生

LLVMを使用してIRを生成する前に、スクリプト内のすべての変数の型を知っておく必要があります。このステップは型推論です。DolphinDBのJITで使用される型控除方法は、次のような部分控除です。

@jit
def foo() {
  x = 1
  y = 1.1
  z = x + y
  return z
}

x = 1を使用してxの型がintであることを判別し、y = 1.1を使用してyの型がdoubleであることを判別します。z= x + yと上記で導出されたxおよびyの型を使用して、zの型を判別します。もdoubleです。returnzを使用して決定しfooます。関数の戻り値の型はdoubleです。

関数に次のようなパラメータがある場合:

@jit
def foo(x) {
  return x + 1
}

foo関数の戻り値の型は、入力値xの型によって異なります。

現在JITでサポートされているデータ型について説明しました。サポートされていない型が関数に表示されるか、入力変数型がサポートされていない場合、関数全体の変数型の派生が失敗し、実行時に例外がスローされます。例えば:

@jit
def foo(x) {
  return x + 1
}

foo(123)             // 正常执行
foo("abc")           // 抛出异常,因为目前不支持STRING
foo(1:2)             // 抛出异常,因为目前不支持pair
foo((1 2, 3 4, 5 6)) // 抛出异常,因为目前不支持tuple

@jit
def foo(x) {
  y = cumprod(x)
  z = y + 1
  return z
}

foo(1..10)             // 抛出异常,因为目前还不支持cumprod函数,不知道该函数返回的类型,导致类型推导失败

したがって、JIT関数を正常に使用できるようにするには、関数またはパラメーターでタプルや文字列などのサポートされていない型を使用しないようにし、まだサポートされていない関数を使用しないでください。

5例

5.1インプライドボラティリティの計算(インプライドボラティリティ)

上記のように、一部の計算はベクトル化できません。インプライドボラティリティの計算は一例です。

@jit
def GBlackScholes(future_price, strike, input_ttm, risk_rate, b_rate, input_vol, is_call) {
  ttm = input_ttm + 0.000000000000001;
  vol = input_vol + 0.000000000000001;

  d1 = (log(future_price/strike) + (b_rate + vol*vol/2) * ttm) / (vol * sqrt(ttm));
  d2 = d1 - vol * sqrt(ttm);

  if (is_call) {
    return future_price * exp((b_rate - risk_rate) * ttm) * cdfNormal(0, 1, d1) - strike * exp(-risk_rate*ttm) * cdfNormal(0, 1, d2);
  } else {
    return strike * exp(-risk_rate*ttm) * cdfNormal(0, 1, -d2) - future_price * exp((b_rate - risk_rate) * ttm) * cdfNormal(0, 1, -d1);
  }
}

@jit
def ImpliedVolatility(future_price, strike, ttm, risk_rate, b_rate, option_price, is_call) {
  high=5.0;
  low = 0.0;

  do {
    if (GBlackScholes(future_price, strike, ttm, risk_rate, b_rate, (high+low)/2, is_call) > option_price) {
      high = (high+low)/2;
    } else {
      low = (high + low) /2;
    }
  } while ((high-low) > 0.00001);

  return (high + low) /2;
}

@jit
def test_jit(future_price, strike, ttm, risk_rate, b_rate, option_price, is_call) {
	n = size(future_price)
	ret = array(DOUBLE, n, n)
	i = 0
	do {
		ret[i] = ImpliedVolatility(future_price[i], strike[i], ttm[i], risk_rate[i], b_rate[i], option_price[i], is_call[i])
		i += 1
	} while(i < n)
	return ret
}

n = 100000
future_price=take(rand(10.0,1)[0], n)
strike_price=take(rand(10.0,1)[0], n)
strike=take(rand(10.0,1)[0], n)
input_ttm=take(rand(10.0,1)[0], n)
risk_rate=take(rand(10.0,1)[0], n)
b_rate=take(rand(10.0,1)[0], n)
vol=take(rand(10.0,1)[0], n)
input_vol=take(rand(10.0,1)[0], n)
multi=take(rand(10.0,1)[0], n)
is_call=take(rand(10.0,1)[0], n)
ttm=take(rand(10.0,1)[0], n)
option_price=take(rand(10.0,1)[0], n)

timer(10) test_jit(future_price, strike, ttm, risk_rate, b_rate, option_price, is_call)          //  2621.73 ms
timer(10) test_non_jit(future_price, strike, ttm, risk_rate, b_rate, option_price, is_call)      //   302714.74 ms

上記の例では、関数ImpliedVolatilityはと呼ばれGBlackScholesます。この関数test_non_jittest_jit、定義の前の@jit削除することで取得できますJITバージョンtest_jit非JITバージョンtest_non_jitより115倍高速に実行されます。

5.2ギリシャ人を計算する

ギリシャ人リスク評価のための定量的ファイナンスよく使用されます。以下では、JITの使用を示す例としてチャームを使用しています。

@jit
def myMax(a,b){
	if(a>b){
		return a
	}else{
		return b
	}
}

@jit
def NormDist(x) {
  return cdfNormal(0, 1, x);
}

@jit
def ND(x) {
  return (1.0/sqrt(2*pi)) * exp(-(x*x)/2.0)
}

@jit
def CalculateCharm(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call) {
  day_year = 245.0;

  d1 = (log(future_price/strike_price) + (b_rate + (vol*vol)/2.0) * input_ttm) / (myMax(vol,0.00001) * sqrt(input_ttm));
  d2 = d1 - vol * sqrt(input_ttm);

  if (is_call) {
    return -exp((b_rate - risk_rate) * input_ttm) * (ND(d1) * (b_rate/vol/sqrt(input_ttm) - d2/2.0/input_ttm) + (b_rate-risk_rate) * NormDist(d1)) * future_price * multi / day_year;
  } else {
    return -exp((b_rate - risk_rate) * input_ttm) * (ND(d1) * (b_rate/vol/sqrt(input_ttm) - d2/2.0/input_ttm) - (b_rate-risk_rate) * NormDist(-d1)) * future_price * multi / day_year;
  }
}

@jit
def test_jit(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call) {
	n = size(future_price)
	ret = array(DOUBLE, n, n)
	i = 0
	do {
		ret[i] = CalculateCharm(future_price[i], strike_price[i], input_ttm[i], risk_rate[i], b_rate[i], vol[i], multi[i], is_call[i])
		i += 1
	} while(i < n)
	return ret
}


def ND_validate(x) {
  return (1.0/sqrt(2*pi)) * exp(-(x*x)/2.0)
}

def NormDist_validate(x) {
  return cdfNormal(0, 1, x);
}

def CalculateCharm_vectorized(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call) {
	day_year = 245.0;

	d1 = (log(future_price/strike_price) + (b_rate + pow(vol, 2)/2.0) * input_ttm) / (max(vol, 0.00001) * sqrt(input_ttm));
	d2 = d1 - vol * sqrt(input_ttm);
	return iif(is_call,-exp((b_rate - risk_rate) * input_ttm) * (ND_validate(d1) * (b_rate/vol/sqrt(input_ttm) - d2/2.0/input_ttm) + (b_rate-risk_rate) * NormDist_validate(d1)) * future_price * multi / day_year,-exp((b_rate - risk_rate) * input_ttm) * (ND_validate(d1) * (b_rate/vol/sqrt(input_ttm) - d2/2.0/input_ttm) - (b_rate-risk_rate) * NormDist_validate(-d1)) * future_price * multi / day_year)
}

n = 1000000
future_price=rand(10.0,n)
strike_price=rand(10.0,n)
strike=rand(10.0,n)
input_ttm=rand(10.0,n)
risk_rate=rand(10.0,n)
b_rate=rand(10.0,n)
vol=rand(10.0,n)
input_vol=rand(10.0,n)
multi=rand(10.0,n)
is_call=rand(true false,n)
ttm=rand(10.0,n)
option_price=rand(10.0,n)

timer(10) test_jit(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call)                     //   1834.342 ms
timer(10) test_none_jit(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call)                // 224099.805 ms
timer(10) CalculateCharm_vectorized(future_price, strike_price, input_ttm, risk_rate, b_rate, vol, multi, is_call)    //   3117.761 ms

上記はより複雑な例であり、より多くの関数呼び出しとより複雑な計算が含まれます。JITバージョンは非JITバージョンよりも約121倍高速で、ベクトル化バージョンよりも約0.7倍高速です。

5.3ストップロス(ストップロス)の計算

このZhihuコラムでは、技術的な信号のバックテストにDolphinDBを使用する方法を示します。以下では、JITを使用してストップロス機能を実装します。

@jit
def stoploss_JIT(ret, threshold) {
	n = ret.size()
	i = 0
	curRet = 1.0
	curMaxRet = 1.0
	indicator = take(true, n)

	do {
		indicator[i] = false
		curRet *= (1 + ret[i])
		if(curRet > curMaxRet) { curMaxRet = curRet }
		drawDown = 1 - curRet / curMaxRet;
		if(drawDown >= threshold) {
			i = n // break is not supported for now
		}
		i += 1
	} while(i < n)

	return indicator
}

def stoploss_no_JIT(ret, threshold) {
	n = ret.size()
	i = 0
	curRet = 1.0
	curMaxRet = 1.0
	indicator = take(true, n)

	do {
		indicator[i] = false
		curRet *= (1 + ret[i])
		if(curRet > curMaxRet) { curMaxRet = curRet }
		drawDown = 1 - curRet / curMaxRet;
		if(drawDown >= threshold) {
			i = n // break is not supported for now
		}
		i += 1
	} while(i < n)

	return indicator
}

def stoploss_vectorization(ret, threshold){
	cumret = cumprod(1+ret)
 	drawDown = 1 - cumret / cumret.cummax()
	firstCutIndex = at(drawDown >= threshold).first() + 1
	indicator = take(false, ret.size())
	if(isValid(firstCutIndex) and firstCutIndex < ret.size())
		indicator[firstCutIndex:] = true
	return indicator
}
ret = take(0.0008 -0.0008, 1000000)
threshold = 0.10
timer(10) stoploss_JIT(ret, threshold)              //      58.674 ms
timer(10) stoploss_no_JIT(ret, threshold)           //   14622.142 ms
timer(10) stoploss_vectorization(ret, threshold)    //     151.884 ms

ストップロス関数は、実際にはドローダウンがしきい値よりも大きい最初の日を見つけるだけでよく、すべてのcumprodとcummaxを計算する必要がないため、JITバージョンはベクトル化バージョンの約1.5倍、約248倍高速です。非JITバージョンより。

データの最終日にストップロスが必要な場合、JITバージョンの速度はベクトル化の速度と同じになりますが、非JITバージョンよりもはるかに高速です。

6.未来

以降のバージョンでは、次の機能を段階的にサポートする予定です。

  1. ブレークをサポートし、do-whileステートメントを続行します。
  2. 辞書などのデータ構造と文字列などのデータ型をサポートします。
  3. より多くの数学関数と統計関数をサポートします。
  4. 強化された型推論関数。DolphinDB組み込み関数によって返されるより多くのデータ型を識別できます。
  5. カスタム関数での入力パラメーター、戻り値、ローカル変数のデータ型の宣言をサポートします。

7まとめ

DolphinDBは、カスタム関数のジャストインタイムコンパイルと実行の機能を開始しました。これにより、forループ、whileループ、if-elseステートメントの実行速度が大幅に向上します。ベクトル化された操作を使用できないシナリオに特に適していますが、実行速度は非常に高速です。たとえば、高周波係数の計算、リアルタイムのストリーミングデータ処理などです。

おすすめ

転載: blog.csdn.net/qq_41996852/article/details/112002998