私はJavaから非常に頻繁に計算する必要がありますが、ネイティブバージョンをJavaMath.exp()
よりも高速に実行することは可能ですか?Math.exp()
jni + Cだけを試しましたが、単純なjavaよりも低速です。
私はJavaから非常に頻繁に計算する必要がありますが、ネイティブバージョンをJavaMath.exp()
よりも高速に実行することは可能ですか?Math.exp()
jni + Cだけを試しましたが、単純なjavaよりも低速です。
これはすでに数回要求されています (たとえば、こちらを参照してください)。このブログ投稿からコピーした Math.exp() の近似値を次に示します。
public static double exp(double val) {
final long tmp = (long) (1512775 * val + (1072693248 - 60801));
return Double.longBitsToDouble(tmp << 32);
}
これは基本的に、2048 エントリのルックアップ テーブルとエントリ間の線形補間と同じですが、これはすべて IEEE 浮動小数点のトリックを使用しています。私のマシンでは Math.exp() よりも 5 倍高速ですが、-server を指定してコンパイルすると、これは大幅に変化する可能性があります。
+1 独自の exp() 実装を作成します。つまり、これが実際にアプリケーションのボトルネックである場合です。少しの不正確さに対処できれば、非常に効率的な指数推定アルゴリズムが数多く存在し、その中には何世紀も前のものもあります。私が理解しているように、Java の exp() 実装は、「正確な」結果を返さなければならないアルゴリズムであっても、かなり遅いです。
ああ、その exp() 実装を pure-Java で書くことを恐れないでください。JNI には多くのオーバーヘッドがあり、JVM は実行時に C/C++ が達成できる範囲を超えてバイトコードを最適化できます。
Javaを使用します。
また、expの結果をキャッシュすると、再度計算するよりも速く答えを調べることができます。
Math.exp()
Cで呼び出しているループもすべてラップする必要があります。そうしないと、JavaとCの間のマーシャリングのオーバーヘッドが、パフォーマンス上の利点を圧倒します。
バッチで実行すると、より高速に実行できる場合があります。JNI 呼び出しを行うとオーバーヘッドが追加されるため、計算する必要がある exp() ごとにそれを行う必要はありません。100 個の値の配列を渡して結果を取得し、パフォーマンスに役立つかどうかを確認します。
本当の問題は、これがボトルネックになっていませんか? アプリケーションのプロファイリングを行った結果、これが速度低下の主な原因であることがわかりましたか?
そうでない場合は、Java のバージョンを使用することをお勧めします。開発が遅くなるだけなので、事前に最適化しないでください。問題ではないかもしれない問題に長い時間を費やすかもしれません。
そうは言っても、あなたのテストはあなたに答えを与えたと思います。jni + C の方が遅い場合は、Java のバージョンを使用します。
Commons Math3には、最適化されたバージョンが付属しています: FastMath.exp(double x)
. それは私のコードを大幅にスピードアップしました。
Fabienがいくつかのテストを実行したところ、次のようにほぼ 2 倍高速であることがわかりましたMath.exp()
。
0.75s for Math.exp sum=1.7182816693332244E7
0.40s for FastMath.exp sum=1.7182816693332244E7
Javadoc は次のとおりです。
exp(x) を計算します。関数の結果はほぼ丸められます。入力値の 99.9% の理論値に正しく丸められます。それ以外の場合、1 UPL エラーが発生します。
方法:
Lookup intVal = exp(int(x))
Lookup fracVal = exp(int(x-int(x) / 1024.0) * 1024.0 );
Compute z as the exponential of the remaining bits by a polynomial minus one
exp(x) = intVal * fracVal * (1 + z)
精度: 計算は 63 ビットの精度で行われるため、結果は入力値の 99.9% に対して正しく丸められる必要があり、それ以外の場合は 1 ULP エラー未満になります。
Java コードは Just-In-Time (JIT) コンパイラでネイティブ コードにコンパイルされるため、JNI を使用してネイティブ コードを呼び出す理由はまったくありません。
また、入力パラメーターが浮動小数点実数であるメソッドの結果をキャッシュしないでください。時間内に得られる利益は、使用されるスペースの量で非常に失われます。
JNI を使用する際の問題は、JNI の呼び出しに伴うオーバーヘッドです。最近の Java 仮想マシンはかなり最適化されており、組み込みの Math.exp() への呼び出しは、C の exp() 関数を直接呼び出すように自動的に最適化されます。指示。
JNI の使用に関連する単純なオーバーヘッドがあります。http: //java.sun.com/docs/books/performance/1st_edition/html/JPNativeCode.fm.htmlも参照してください。
したがって、他の人が提案しているように、JNI の使用を伴う操作を照合してみてください。
必要に応じて、独自に作成します。
たとえば、すべての指数が 2 の累乗である場合、ビットシフトを使用できます。限られた範囲または一連の値を使用する場合は、ルックアップ テーブルを使用できます。ピンポイントの精度が必要ない場合は、不正確ですが高速なアルゴリズムを使用します。
JNI 境界を越えた呼び出しに関連するコストがあります。
exp() を呼び出すループもネイティブ コードに移動して、ネイティブ呼び出しが 1 つだけになるようにすると、より良い結果が得られる可能性がありますが、純粋な Java ソリューションよりも大幅に高速になるとは思えません。
アプリケーションの詳細はわかりませんが、呼び出しで使用できる引数のセットがかなり限られている場合は、事前に計算されたルックアップ テーブルを使用して Java コードを高速化できます。
あなたが達成しようとしていることに応じて、expのより高速なアルゴリズムがあります。問題空間が特定の範囲に制限されているか、特定の解像度、精度、または精度のみが必要かなど。
問題を非常にうまく定義すると、たとえば、補間を使用してテーブルを使用できることがわかる場合があります。これにより、他のほとんどすべてのアルゴリズムが水から吹き飛ばされます。
そのパフォーマンスのトレードオフを得るために exp に適用できる制約は何ですか?
-アダム
もう関係ないかもしれませんが、OpenJDK の最新リリース (こちらを参照) では、Math.exp を組み込みにする必要があります (それが何かわからない場合は、こちらを参照してください)。
Hotspot VM が Math.exp への呼び出しを、実行時に exp のプロセッサ固有の実装に置き換えることを意味するため、これにより、ほとんどのアーキテクチャでパフォーマンスが無敵になります。アーキテクチャに最適化されているため、これらの呼び出しに勝るものはありません...
フィッティング アルゴリズムを実行すると、フィッティング結果の最小誤差が Math.exp() の精度よりもはるかに大きくなります。
超越関数は常に加算や乗算よりもはるかに遅く、よく知られているボトルネックです。値が狭い範囲にあることがわかっている場合は、単純にルックアップ テーブルを作成できます (2 つの並べ替えられた配列、1 つの入力、1 つの出力)。Arrays.binarySearch を使用して正しいインデックスを見つけ、[index] と [index+1] の要素で値を補間します。
別の方法は、数を分割することです。たとえば、3.81 を 3+0.81 に分割します。e = 2.718 を 3 回掛けると 20.08 になります。
0.81になりました。0 から 1 の間のすべての値は、よく知られている指数級数で高速に収束します
1+x+x^2/2+x^3/6+x^4/24....など
精度のために必要なだけ多くの用語を使用してください。残念ながら、x が 1 に近づくと遅くなります。x^4 に移動すると、正しい 2.2448 ではなく 2.2445 が得られます。
次に、結果 2.781^3 = 20.08 を 2.781^0.81 = 2.2445 で乗算すると、2000 分の 1 の誤差で結果 45.07 が得られます (正解: 45.15)。