【问题标题】:JavaScript fractal generation algorithms - why is one so much faster?JavaScript 分形生成算法 - 为什么这么快?
【发布时间】:2022-01-20 23:08:51
【问题描述】:

我正在尝试根据第一原理编写 JavaScript 分形生成算法。我知道那里有很多示例,但我想结合额外的功能来支持 Mandelbrot 和“旋转”Julia,以及“Burning Ship”和“Tricorn”等变体。考虑到这一点,我实现了一个轻量级的 Complex 数学库(同样,我知道那里有标准的 Complex js 库,但我想从头开始构建一个作为学习练习)。

我测试了两个替代函数,一个使用标准数学函数fractal,另一个使用我的复杂库方法fractalComplex。它们都运行良好,但我惊讶地发现标准版本几乎是复杂版本的 两倍。我期待一些额外的开销,但没有那么多!

谁能解释为什么? Complex 库“在幕后”使用相同的数学结构。额外的开销是否完全取决于对象创建?

代码复制如下(输入参数 z 和 c 是 {re, im} 形式的对象)。

function fractal(z, c, maxiter) {

    var i, za, re, im, re2, im2;
    c = (settype === JULIA ? c : z);

    // Iterate until abs(z) exceeds escape radius
    for (i = 0; i < maxiter; i += 1) {

        if (setvar === BURNING_SHIP) {
            re = Math.abs(z.re);
            im = -Math.abs(z.im);
        }
        else if (setvar === TRICORN) {
            re = z.re
            im = -z.im; // conjugate z
        }
        else { // Mandelbrot
            re = z.re;
            im = z.im;
        }

        re2 = re * re;
        im2 = im * im;
        z = { // z = z² + c
            re: re2 - im2 + c.re,
            im: 2 * im * re + c.im
        };

        za = re2 + im2 // abs(z)²
        if (za > 4) { // abs(z)² > radius²
            break;
        }
    }
    za = Math.sqrt(za); // abs(z)
    return { i, za };
}

function fractalComplex(z, c, maxiter, n, radius) {

    var i, za;
    c = (settype === JULIA ? c : z);

    // Iterate until abs(z) exceeds escape radius
    for (i = 0; i < maxiter; i += 1) {

        if (setvar === BURNING_SHIP) {
            z = new Complex(Math.abs(z.re), -Math.abs(z.im))
        }
        if (setvar === TRICORN) {
            z = z.conjugate()
        }

        z = z.quad(n, c); // z = zⁿ + c
        za = z.abs();
        if (za > radius) {
            break;
        }
    }
    return { i, za };
}

我的“复杂精简版”库如下:

// ------------------------------------------------------------------------
// A basic complex number library which implements the methods used for
// Mandelbrot and Julia Set generation.
// ------------------------------------------------------------------------
'use strict';

// Instantiate complex number object.
function Complex(re, im) {
  this.re = re; // real
  this.im = im; // imaginary
}

Complex.prototype = {

  're': 0,
  'im': 0,

  // Set value.
  'set': function (re, im) {
    this.re = re;
    this.im = im;
  },

  // Get magnitude.
  'abs': function () {
    return Math.sqrt(this.re * this.re + this.im * this.im);
  },

  // Get polar representation (r, θ); angle in radians.
  'polar': function () {
    return { r: this.abs(), θ: Math.atan2(this.im, this.re) };
  },

  // Get square.
  'sqr': function () {
    var re2 = this.re * this.re - this.im * this.im;
    var im2 = 2 * this.im * this.re;
    return new Complex(re2, im2);
  },

  // Get complex number to the real power n.
  'pow': function (n) {
    if (n === 0) { return new Complex(1, 0); }
    if (n === 1) { return this; }
    if (n === 2) { return this.sqr(); }
    var pol = this.polar();
    var rn = Math.pow(pol.r, n);
    var θn = n * pol.θ;
    return cart(rn, θn);
  },

  // Get conjugate.
  'conjugate': function () {
    return new Complex(this.re, -this.im);
  },

  // Get quadratic zⁿ + c.
  'quad': function (n, c) {
    var zn = this.pow(n);
    return new Complex(zn.re + c.re, zn.im + c.im);
  },

  // Rotate by angle in radians.
  'rotate': function (angle) {
    var pol = this.polar();
    angle += pol.θ;
    return new Complex(pol.r * Math.cos(angle), pol.r * Math.sin(angle));
  },

  // String in exponent format to specified significant figures.
  'toString': function (sig = 9) {
    return this.re.toExponential(sig) + " + " + this.im.toExponential(sig) + "i";
  },
}

// Convert polar (r, θ) to cartesian representation (re, im).
function cart(r, θ) {
  var re = r * Math.cos(θ);
  var im = r * Math.sin(θ);
  return new Complex(re, im);
}

补充编辑 22/12/2021 11:52:

对于它的价值,这就是我最终决定的......

   function fractal(p, c, n, maxiter, radius) {

        var i, za, zre, zim, tre, cre, cim, r, θ;
        var lastre = 0;
        var lastim = 0;
        var per = 0;
        if (setmode === JULIA) {
            cre = c.re;
            cim = c.im;
            zre = p.re;
            zim = p.im;
        }
        else { // Mandelbrot mode
            cre = p.re;
            cim = p.im;
            zre = 0;
            zim = 0;
        }

        // Iterate until abs(z) exceeds escape radius
        for (i = 0; i < maxiter; i += 1) {

            if (setvar === BURNING_SHIP) {
                zre = Math.abs(zre);
                zim = -Math.abs(zim);
            }
            else if (setvar === TRICORN) {
                zim = -zim; // conjugate z
            }

            // z = z² + c
            if (n == 2) {
                tre = zre * zre - zim * zim + cre;
                zim = 2 * zre * zim + cim;
                zre = tre;
            }
            else { // z = zⁿ + c, where n is integer > 2
                r = powi(Math.sqrt(zre * zre + zim * zim), n); // radiusⁿ
                //r = Math.pow(Math.sqrt(zre * zre + zim * zim), n); // radiusⁿ
                θ = n * Math.atan2(zim, zre); // angleⁿ
                zre = r * Math.cos(θ) + cre;
                zim = r * Math.sin(θ) + cim;
            }

            // Optimisation - periodicity check speeds
            // up processing of points within set
            if (PERIODCHECK) {
                if (zre === lastre && zim === lastim) {
                    i = maxiter;
                    break;
                }
                per += 1;
                if (per > 20) {
                    per = 0;
                    lastre = zre;
                    lastim = zim;
                }
            }
            // ... end of optimisation

            za = zre * zre + zim * zim // abs(z)²
            if (za > radius) { // abs(z)² > radius²
                break;
            }
        }
        return { i, za };
    }

    // Optimised pow() function for integer exponents
    // using 'halving and squaring'.
    function powi(base, n) {

        var res = 1;
        while (n) {
            if (n & 1) { // if n is odd
                res *= base;
            }
            n >>= 1; // n * 2
            base *= base;
        }
        return res;
    }

【问题讨论】:

  • 你用什么引擎来运行代码,你尝试了多少次迭代?
  • @Bergi 我已经在 Firefox 浏览器 (95.0.1) 和 Node.js 16.13.1 下本地运行它(目的是部署为网站或电子应用程序) .对于 maxiter,我使用了 100 的固定 maxiter 和自动增加缩放级别的 maxiter 的算法(见下文)。我在所有情况下都看到相同的性能差异:function getAutoiter(zoom) { return Math.max(MAXITER, parseInt(Math.abs(1000 * Math.log(1 / Math.sqrt(zoom))))); }
  • @Bergi 给你一些指标;基于 750 x 500 像素的画布,我看到 fractal 版本的总执行时间约为 190 毫秒,fractalComplex 版本的总执行时间为 280 毫秒(其余代码相同)。
  • "Complex 库“在幕后”使用相同的数学结构。" 请澄清此语句的含义,因为 fractalfractalComplex 不仅具有不同的参数,而且后者中的附加 radius 参数参与打破for 循环的逻辑...加上z = z**2 + cfractal 计算在fractalComplex 中被替换为对quad 的调用,这然后调用pow,后者又调用polarcart...即,在fractal 计算中似乎没有类似的极坐标和笛卡尔坐标转换...
  • pow,真正的变体,在大多数数学库中是一个相当复杂的过程。 exp(y*ln(x)) 不够准确。使用减半和平方的整数幂低于 5,甚至可能低于 10,速度更快。

标签: javascript performance fractals


【解决方案1】:

以下类可能同时满足性能问题和对复杂对象的适当封装...

知名人士:

  • 在适用的情况下,始终返回this Complex 对象(即实例化对象),这有助于链接。例如,

    • x = new Complex(10, 5).sqrThis().powThis(1);
  • 对于每个返回 Complex 对象的 Complex 方法,配置两 (2) 个方法:

    • 一种名为&lt;method&gt;This 的方法,它直接对this 对象进行操作并包含函数逻辑。
    • 一种名为&lt;method&gt; 的方法,它从this 对象中克隆一个新的Complex 对象,然后调用&lt;method&gt;This 在克隆上执行函数逻辑。
    • 这使开发人员可以选择更新现有对象或返回新对象的方法。
  • 对其他复杂方法的内部调用通常应使用&lt;method&gt;This 版本,因为初始调用确定是就地使用现有this 对象,还是克隆它。从那里开始,对其他 Complex 方法的所有内部调用将继续在 this 对象或克隆上运行。

// ------------------------------------------------------------------------
// A basic complex number library which implements the methods used for
// Mandelbrot and Julia Set generation.
// ------------------------------------------------------------------------
'use strict';

class Complex {

  constructor( reOrComplex, im ) {
    this.set( reOrComplex, im );
  }
  
  set( reOrComplex, im ) {
    if ( reOrComplex instanceof Complex ) {
      this.re = reOrComplex.re;
      this.im = reOrComplex.im;
    } else {
      this.re = reOrComplex;
      this.im = im;
    }
    return this;
  }
  
  abs() {
    return Math.sqrt(this.re * this.re + this.im * this.im);
  }

  toPolar() {
    return { r: this.abs(), θ: Math.atan2(this.im, this.re) };
  }

  sqrThis() {
    return this.set( this.re * this.re - this.im * this.im, 2 * this.im * this.re );
  }
  
  sqr() {
    return new Complex( this ).sqrThis();
  }

  powThis( n ) {
    if ( n === 0 ) { return this.set( 1, 0 ) };
    if ( n === 1 ) { return this; }
    if ( n === 2 ) { return this.sqrThis(); }
    let polar = this.toPolar();
    return this.toCartesianThis( Math.pow(polar.r, n), n * polar.θ );
  }
  
  pow( n ) {
    return new Complex( this ).powThis( n );
  }

  conjugateThis() {
    return this.set( this.re, -this.im);
  }
  
  conjugate() {
    return new Complex( this ).conjugateThis();
  }

  quadraticThis( n, c ) {
    let zn = this.powThis( n );
    return this.set( zn.re + c.re, zn.im + c.im );
  }

  quadratic( n, c ) {
    return new Complex( this ).quadraticThis( n, c );
  }

  rotateThis( deltaAngle ) {
    let polar = this.toPolar();
    let angle = polar.θ + deltaAngle;
    return this.set( polar.r * Math.cos(angle), polar.r * Math.sin(angle) );
  }
  
  rotate( deltaAngle ) {
    return new Complex( this ).rotateThis( deltaAngle );
  }

  toString( sig = 9 ) {
    return this.re.toExponential( sig ) + " + " + this.im.toExponential( sig ) + "i";
  }
  
  toCartesianThis( r, θ ) {
    return this.set( r * Math.cos( θ ), r * Math.sin( θ ) );
  }
}

// Convert polar (r, θ) to cartesian representation (re, im).
Complex.toCartesian = function ( r, θ ) {
  return new Complex().toCartesianThis( r, θ );
}


let x = new Complex( 10, 5 ).sqrThis();
console.log( 'x = new Complex( 10, 5 ).sqrThis()' );
console.log( 'x is ', x );
let y = x.pow( 3 );
console.log ( 'y = x.pow( 3 )' );
console.log ( 'y is ', y );
x.sqr();
console.log ( 'x.sqr()' );
console.log ( 'x is still', x );
x.sqrThis();
console.log ( 'x.sqrThis()' );
console.log ( 'x is now', x );

简而言之,以这种方式构造一个类提供了相同方法的两个版本:

  • 一种体现函数逻辑并直接改变实例化的this对象的方法。
  • 另一个方法是简单地克隆实例化的this 对象,然后调用包含函数逻辑的关联方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-02-07
    • 1970-01-01
    • 2020-03-22
    • 1970-01-01
    • 2011-03-27
    • 2015-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多