はじめに
この記事は、マンデルブロ集合の面積を求めることをテーマに、計算速度を測ったり、piとeとiについて、調べることを目的とした実験の一部です。マンデルブロ集合の周囲の長さは無限大ですが、面積は約1.5の無理数と決まっているので、題材として扱いやすいのです。
アルゴリズムの選定をしたのち、マルチスレッド化等の高速化をしました。
面積の解
サイトによると√(6π-1)-e
\sqrt{6\pi-1}-e
で求まるようだが、解析解なのか、近似式なのか?
エクセル式は
=SQRT(6*PI()-1)-EXP(1)
1.506591651485500000000000000000
wolframによる計算結果は1.5065916514855032852705345234769657413403164236938572609274770450
mrob.comによるとGPUでカウントして
1.5065918561
小数点以下6桁が一致
自作プログラムでは、
1.5066597
小数点以下3桁が一致
processingサンプルのプログラムは遅い?
processingにはマンデルブロ集合を描くサンプルが入っている。しかし、平方根の計算が入っていて、なんだか遅そう。そこでマンデルブロ集合の面積(ピクセル数カウント)を計算しながら検証。
条件
M1Macで実行。
サイズ 512x512 で 65536回の設定で計算。
結果が63277ピクセルであることを確認。
結果
①サンプルをいじって、必要な部分だけを残して計算。
25.7秒
②平方根のないコード
14.7秒
結論
やっぱり標準サンプルのコードは遅く、1.75倍の差が出ました。平方根関数が遅いだけでなく、floatしか扱えないので精度問題も残ります。②のコードでは、floatで計算してもdoubleで計算しても、速度に違いが出ませんでした。なぜか?(追記:コード中の数字にdを明記した)
検証コード
size(512, 512);
noLoop();
background(255);
float w = 2.5;
float h = 2.5;
float xmin = -2.0;
float ymin = -1.25;
int maxiterations = 65536;
int count = 0;
float xmax = xmin + w;
float ymax = ymin + h;
float dx = (xmax - xmin) / (width);
float dy = (ymax - ymin) / (height);
float y = ymin;
for (int j = 0; j < height; j++) {
float x = xmin;
for (int i = 0; i < width; i++) {
float a = x;
float b = y;
int n = 0;
float max = 4.0;
while (n < maxiterations) {
float aa = a * a;
float bb = b * b;
float abs = sqrt(aa + bb);
if (abs > max) {
break;
}
float twoab = 2.0 * a * b;
a = aa - bb + x;
b = twoab + y;
n++;
}
if (n == maxiterations) {
count++;
}
x += dx;
}
y += dy;
}
println(count);
println(millis());
double w = 2.5;
double h = 2.5;
double xmin = -2.0;
double ymin = -1.25;
double xmax = xmin + w;
double ymax = ymin + h;
double dx = (xmax - xmin) / 512;
double dy = (ymax - ymin) / 512;
int maxiteration = 65536;
int count = 0;
void setup() {
size(512, 512);
noLoop();
double y = ymin;
for (int j = 0; j < 512; j++) {
double x = xmin;
for (int i = 0; i < 512; i++) {
count += mandelbrotJulia(0, 0, x, y, maxiteration);
x += dx;
}
y += dy;
}
println(count);
println(millis());
}
void draw() {
}
int mandelbrotJulia(double zr, double zi,
double cr, double ci,
int iteration) {
for (int n=0; n<iteration; n++) {
double tmp;
tmp = zr*zr - zi*zi + cr;
zi = 2*zr*zi + ci;
zr = tmp;
if (zr*zr + zi*zi>4) {
return 0;
}
}
return 1;
}
スレッドで高速化
速い方のコードで、マルチスレッド化した。processingではthreadの機能があるが、値を渡せないので使わず。過去にjavaのRunnableを使ったが、値が若干おかしくなって、直せなかったので、今回は使わず。
Runnableではなく、値を渡して返すことも可能なCallableを使った。計算したい範囲などの値を渡して、発散・非発散かを返す。とりあえず、64スレッドに分けて、同時に実行。無駄が生じるが、無視。
また収束することが明らかな、カージオイドといくつかの円を計算から除外した。
結果
M1Macで65536x65536pixelで、反復65536で40分ぐらい。面積は1.5066597
コード
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;
void setup() {
size(512, 512);
noLoop();
println("div,iter,pixels,area,millis");
double[] w = new double[64];
double[] h = new double[64];
double[] xmin = new double[64];
double[] ymin = new double[64];
for (int i=0; i<64; i++) {
w[i] = 2.5d/8.0d;
h[i] = 2.5d/8.0d;
xmin[i] = -2.0d+2.5d/8.0d*(i%8);
ymin[i] = -1.25d+2.5d/8.0d*(i/8);
}
for (int div=16384; div<=65536/4; div*=2) {
for (int iter=16384; iter<=65536/4; iter*=2) {
//スレッドの実行
ExecutorService[] ex = new ExecutorService[64];
Future<Integer>[] futureResult = new Future[64];
for (int i=0; i<64; i++) {
ex[i] = Executors.newSingleThreadExecutor();
futureResult[i] = ex[i].submit(new calcArea(w[i], h[i], xmin[i], ymin[i], div/8, iter));
}
//結果の取得
try {
int result = 0;
for (int i=0; i<64; i++) {
result += futureResult[i].get();
}
//println(result);
print(div+",");
print(iter+",");
print(result+",");
print(result*2.5d*2.5d/div/div+",");
println(millis()+"msec");
}
catch (Exception e) {
System.out.println(e);
}
}
}
}
void draw() {
}
public class calcArea implements Callable<Integer> {
double xmax, ymax, dx, dy;
double w, h, xmin, ymin, size;
int maxIteration;
int areaPixels = 0;
public Integer call() {
double y = ymin;
for (int j = 0; j < size; j++) {
double x = xmin;
for (int i = 0; i < size; i++) {
if (outOfCardioidCircles(x, y) ) {
areaPixels += mandelbrotJulia(0d, 0d, x, y, maxIteration);
} else {
areaPixels++;
}
x += dx;
}
y += dy;
}
return areaPixels;
}
public calcArea(double _w, double _h, double _xmin, double _ymin, int _size, int _maxIteration) {
w = _w;
h = _h;
xmin = _xmin;
ymin = _ymin;
size = _size;
maxIteration = _maxIteration;
xmax = xmin + w;
ymax = ymin + h;
dx = (xmax - xmin) / size;
dy = (ymax - ymin) / size;
}
int mandelbrotJulia(double zr, double zi,
double cr, double ci,
int iteration) {
for (int n=0; n<iteration; n++) {
double tmp;
tmp = zr*zr - zi*zi + cr;
zi = 2d*zr*zi + ci;
zr = tmp;
if (zr*zr + zi*zi>4d) {
return 0;
}
}
return 1;
}
}
boolean outOfCardioidCircles(double ix, double iy) {
//カージオイド
double cx = -ix+0.25d;
double cy = iy;
double a =0.5d;
//円
double cx0 = -1.d-ix;
double cy0 = 0d-iy;
double r0 = 0.25d;
double cx1 = -1.309d-ix;
double cy1 = 0d-iy;
double r1 = 0.055d;
double cx2 = -0.125d-ix;
double cy2 = -0.744d-iy;
double r2 = 0.09d;
double cx3 = -0.125d-ix;
double cy3 = 0.744d-iy;
double r3 = 0.09d;
if (
(cx*cx+cy*cy)*(cx*cx+cy*cy-2.*a*cx)-a*a*cy*cy < 0d ||
cx0*cx0+cy0*cy0 < r0*r0 ||
cx1*cx1+cy1*cy1 < r1*r1 ||
cx2*cx2+cy2*cy2 < r2*r2 ||
cx3*cx3+cy3*cy3 < r3*r3
) {
return false;
} else {
return true;
}
}