Spring WebFlux事始め
昨年中頃からSpring WebFluxの勉強を始めたが、結構敷居が高く、なかなか思うように学習が捗らなくなってしまった。 もともと私は科学技術計算を専門にしていたので、数値計算をWebFlux化してみたら少しは理解が進むかもと考えるようになり、 とにかくできるところから、敷居の低いところから始めることにしようと思い至った次第である。
およそ十数年前にコンピュータ足し算の誤差をなくす方法を考えたことがあった。 その当時作成したJavaプログラムが今も残っており、なんと最新のJavaコンパイラで正常に動作することがわかった。 昨年ブログ化した「ノンブロッキングI/Oプログラミング」ではこのプログラムを使っている。 せっかく作ったのにこのままお蔵入りしてもつまらない、また、興味ある人にこういうものもあると知ってもらいたい、と思い、この場でプログラムの公開を行なうことにした。 まず手始めに、これがどういうプログラムかざっと解説しようと思う。
1 マルコムのカスケーディングアキュームレータ
数値計算の中で無誤差加算というものがあり、マルコムのカスケーディングアキュームレータ(Malcolm's Cascading Accumulator)として良く知られる計算法がある。 1970年代の大型コンピュータ黎明期の時代、単精度浮動小数計算が主流だった時代(今は倍精度が主流)、多数データの合計計算の誤差が無視できないほど大きかった。 マルコムは、大きいものは大きいもの同士、小さいものは小さいもの同士で加算することで誤差をなくす方法を発表した。 その後、倍精度計算が主流になり、誤差が激減したためマルコムの方法は主流にはならなかったが、誤差を減らす計算の研究は着々と行なわれている。 有効精度はその計算の精度なのだから無誤差加算なんて無意味と主張する意見もあるが、数値計算の様々な分野で応用されている。
マルコムの方法は倍精度計算にも応用され、倍精度データ(64ビット)を半分にして単精度データ(32ビット)を2つ作り、それを倍精度アキュームレータに足し込んでいく方法が提示された。 8を基数とする倍精度アキュームレータはいくらか大きな配列になるので、メモリが少ない時代はあまり歓迎されていなかったと思われる。 しかし、現代のPCでさえ数ギガから数十ギガのオーダーのメモリ搭載が普通になり、数キロバイト程度のアキュームレータは大したサイズではなくなった。 マルコムの方法は約1600万回の加算で誤差が発生するが、リフレッシュしながら加算すれば、無限に加算できるようになる。 リフレッシュはいたって簡単で、アキュームレータの内部配列データをいったん外に出し、0に初期化後に加算していくだけで良い。
エクセルなどの表計算で経験したことがある人もあると思うが、巨大な値に極小値を加算するような計算を繰り返し行うと計算結果が著しく狂ってしまうことがある。 こういう場合、マルコムのカスケーディングアキュームレータは強力な性能を発揮することができる。
ずいぶん昔にマルコムのカスケーディングアキュームレータを自作し、国内で学会発表した。大した吟味もせずに海外の有名論文に投稿してみたら、予想通り不採用になってしまった。 その当時、精度保証計算が主流だったので、無誤差加算はどうも過去の遺物的存在だったようだ。 年齢的にも気力が無かったのでそのまま放置していたが、せっかく作ったものを捨て去るのはどうかとも考え、この場を借りて公表することにした。
2 Spring WebFluxによるマルチコア対応プログラム
以前、ノンブロッキングI/Oプログラミングの考察でマルチコア対応の数値計算プログラムの紹介をおこなった。 積分計算の範囲を分割し、それぞれを別のスレッドで行なうことでマルチコア計算を実現したが、それをWebFluxの並列処理に書き換えることにする。 一般的に台形公式の積分計算は、分割数が大きすぎると誤差が堆積するので、通常計算での利用はあまりお勧めできない。 計算時間がかかりすぎることと、積分の解を使って計算できるならそちらのほうがはるかに優れている。 しかしながら、今回の計算は無誤差加算の実力を示すために行なったものであることをご留意いただきたい。 理論的に解がπとなる積分を行なっているので、結果が正しいかどうかの判定が容易である。 実際に計算を行うと、16桁の有効精度(double)で計算したものが、分割数をかなり大きくすると計算結果は20桁の有効精度でπの値と一致した。 (計算結果をdoubleで出力するとdoubleの有効精度しかないが、4倍精度で出力することもできる) 不思議だが、本当の話である。
// 積分∫(0to1){4/(1+x*x)}dx を台形公式で求める
package com.example.demo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import com.example.sum.*;
public class calc_MultiCore
{
static final double PI = Math.PI;
public static Mono<String> calc(int div, int multi)
{
long n = Power.pow(2L,div); // 台形公式の分割数
int N_thread = Power.pow(2, multi); // スレッド数
long n_div = n/N_thread;
double h = 1.0/n;
SAcc_v1_5 SAcc = new SAcc_v1_5();
return Flux.range(0, N_thread)
.parallel()
.runOn(Schedulers.parallel())
.map(i -> { // 並列処理
long start = i.intValue()*n_div;
if (start == 0) start = 1;
long end = i.intValue()*n_div + n_div;
Malcolm9_CA mc = new Malcolm9_CA();
calc_multi_part(mc,start,end,h);
return mc;
})
.sequential()
.map(mc -> { // 逐次処理
mc.addto(SAcc);
return "";
})
.then(Mono.just("")) // 逐次処理の終了
.map(nw -> { // 最終計算および出力情報の作成
SAcc.add(3.0);
double sumM = SAcc.getSum()*h;
double deltaM = sumM - PI;
String str = " Multicore threads calculation πの計算\n\n";
........... 途中省略 ...........
return str;
});
}
// 並列処理計算
static private void calc_multi_part(
Malcolm9_CA ca, long start, long end, double h) {
for (long i=start; i<end; i++) {
double z = i*h;
ca.add(4.0/(1.0+z*z));
}
}
}
SAcc_v1_5およびMalcolm9_CAはマルコムのカスケーディングアキュームレータを実装したクラスである。 SAcc_v1_5は古くて遅いが機能は多い。Malcolm9_CAは最新で高速だが機能が少ない。 途中でプログラム開発を放棄した結果だが、昔作ったプログラムなので作った本人も忘れてしまっているという情けなさである。 なので、2つのプログラムを併用する結果となった。どちらも同じような計算原理で動作する。 最深ループは高速のMalcolm9_CAを使い、最終段階の合計処理はSAcc_v1_5を使った。 使い方はいたって簡単で、以下のようになる。
a.add(9.5);
a.add(2.1);
double sum = a.getSum(); // = 11.6
SAcc_v1_5 b = new SAcc_v1_5();
b.add(0.8);
a.addto(b);
double sum2 = b.getSum(); // = 12.4
なお、プログラムcalc_MutiCore.javaにおいて、4倍精度の計算結果が欲しい場合は、
のように記述する。ところで、分割数やスレッド数は2のべき乗になっている。 この場合のみ、割り算を実行した場合において完全に割り切れることが保証され、誤差の発生を最大限抑制することができる。
本プログラムのダウンロードおよびその説明は、マルコムのカスケーディングアキュームレータを参照のこと。
コントローラは以下の通り。
package com.example.demo;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@RestController
@Configuration
public class RouterFunctionConfig {
@Bean
@CrossOrigin
RouterFunction<?> router() {
return route(GET("/calc_mcoreSum/{div}/{multi}"), this::calc_mcoreSum);
}
public Mono<ServerResponse> calc_mcoreSum(ServerRequest req) {
int div = Integer.valueOf(req.pathVariable("div"));
int multi = Integer.valueOf(req.pathVariable("multi"));
return ServerResponse.ok().body(calc_MultiCore.calc(div,multi),String.class);
}
}
gradleを使用した場合、
implementation 'org.springframework.boot:spring-boot-starter-webflux'
だけを設定すればよい。Spring Initializerを使った場合、Spring Reactive Webをチェックする。
3 実行結果 - Spring WebFlux 数値計算も高速
今回作成したのはサーバ用のプログラムであり、ブラウザから指定する形で動作する。 計算結果はブラウザに反映するようになっており、複数の利用者に対応するように作られている。 さらに、SpringWebFluxはバックプレッシャーが働き、利用者マシンの状況により、速度を落としたり、キャンセルする機能が自動化されている。
そのような複雑な処理を行なうサーバでの数値計算はいくらか遅くなるだろうと予想していた。 しかし、昨年作成した最高の速度で実行できるように作ったつもりのJavaプログラムよりやや速い計算速度に驚いた。 これならば、計算サーバとしても使えるのではと思ったが、世界の趨勢から取り残されている自分は井戸の中の蛙だろう。 クラウドコンピューティングなどの世界で、もうすでに実践されているに違いない。
4 WebFlux プログラム作成上の注意点
普通のJavaプログラムはオブジェクト配列を使って処理を簡略化したりするが、Spring WebFluxでは厳禁であった。 GC(ガベージコレクション)が働かなくなり、正常動作は最初の一回のみとなる。 その後は不安定になり、処理時間がとんでもなく遅くなる。 Flux.range(start,count)は、startから始まり、1ずつ増加する数字の列をcount数分生成する。 これを使うと、今回のケースでは配列を使う必要がなくなった。
WebFluxは慣れるのに相当時間がかかり、敷居が非常に高いプログラミングだと感じた。 一般的にプログラミングは人によって千差万別だろうが、やっちゃいけないことも多い。 前掲のWebFluxプログラムを見て、初心者だったら目が点になっていることだろう。 だからと言って、プログラムの詳しい説明をするつもりはない。 書いた本人もまだよく理解できていないのだから。しかし、実際には安定動作したので、おそらく間違ってはいない。
作成日: 更新日:
次はマルコムのカスケーディングアキュームレータのダウンロードおよび解説