[开源] 多平台一致精确计算库eMath

原理


小数运算在各平台可能存在差异或不精确的情况,而整数运算没有这个问题,所以只要将所有小数全都转成整数就OK了。

源码

/**
 * @author zp
 */
/**
 * 多平台一致精确计算库,比decimal更轻量更快
 * [Math.round、Math.min、Math.max,Math.floor、Math.ceil,这些系统方法一般情况下是可以放心使用的]
 */
const exactMath = Object.create(null);
module.exports = exactMath;

// 计算精度
// sin、cos、tan方法的误差小数点后16位
const ACCURACY_SIN_ERROR = 1e-16;
const ACCURACY_COS_ERROR = 1e-16;
const ACCURACY_TAN_ERROR = 1e-16;

// 角度弧度常量
const DEG = 57.29577951308232;
const RAD = 0.017453292519943295;

// 系统常量
exactMath.PI = 3.141592653589793;
exactMath.E = 2.718281828459045;
exactMath.LN2 = 0.6931471805599453;
exactMath.LN10 = 2.302585092994046;
exactMath.LOG2E = 1.4426950408889634;
exactMath.LOG10E = 0.4342944819032518;
exactMath.SQRT1_2 = 0.7071067811865476;
exactMath.SQRT2 = 1.4142135623730951;

/**
 * 链式调用
 * @example
 * const value = exactMath.value(10).add(20.123).mul(2).sqrt().value;
 */
let chain = null;
exactMath.value = function (value) {
    if (!chain) {
        chain = {
            value: 0,
            valueOf() { return this.value; },
            toString() { return String(this.value); }
        }
        for (const key in exactMath) {
            if (key !== 'value' && typeof exactMath[key] === 'function') {
                chain[key] = function (...args) {
                    this.value = exactMath[key].call(exactMath, this.value, ...args);
                    return this;
                }
            }
        }
    }

    chain.value = value;
    return chain;
}

/****************************************************基础****************************************************/
/**
 * 获得小数位数
 * @param {Number} num 浮点数
 * @returns {Number}
 */
exactMath.getDecimalPlace = function (num) {
    if (num && num !== Math.floor(num)) {
        for (let n = 1, m = 10, temp = 0; n < 20; n += 1, m *= 10) {
            temp = num * m;
            if (temp == Math.floor(temp)) return n;
        }
        return 20;
    } else {
        return 0;
    }
}
/**
 * 保留n为小数,并四舍五入
 * @example
 * (2.335).toFixed(2)
 * exactMath.toFixed(2.335, 2)
 * @param {Number} num 浮点数
 * @param {Number} n 整数
 * @returns {Number}
 */
exactMath.toFixed = function (num, n = 0) {
    if (n == 0) {
        return Math.round(num);
    } else {
        const m = Math.pow(10, n);
        return Math.round(num * (m * 10) / 10) / m;
    }
}

exactMath.abs = function (x) {
    return Math.abs(x);
}
exactMath.round = function (x) {
    return Math.round(x);
}
exactMath.ceil = function (x) {
    return Math.ceil(x)
}
exactMath.floor = function (x) {
    return Math.floor(x)
}
exactMath.min = function (...args) {
    return Math.min(...args);
}
exactMath.max = function (...args) {
    return Math.max(...args);
}

/**
 * 小数相加
 * @param {Number} num1 浮点数
 * @param {Number} num2 浮点数
 * @returns {Number}
 */
exactMath.add = function (...args) {
    if (args.length === 2) {
        const num1 = args[0];
        const num2 = args[1];
        const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
        return (this.toFixed(num1 * m) + this.toFixed(num2 * m)) / m;
    } else {
        return args.reduce((a, b) => this.add(a, b))
    }
};
/**
 * 小数相减
 * @param {Number} num1 浮点数
 * @param {Number} num2 浮点数
 * @returns {Number}
 */
exactMath.sub = function (...args) {
    if (args.length === 2) {
        const num1 = args[0];
        const num2 = args[1];
        const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));

        return (this.toFixed(num1 * m) - this.toFixed(num2 * m)) / m;
    } else {
        return args.reduce((a, b) => this.sub(a, b))
    }
};
/**
 * 小数相乘
 * @param {Number} num1 浮点数
 * @param {Number} num2 浮点数
 * @returns {Number}
 */
exactMath.mul = function (...args) {
    if (args.length === 2) {
        let num1 = args[0];
        let num2 = args[1];

        // 方案1:
        // 直接相乘,但是相乘两数小数点过多会导致中间值[(n1 * m1) * (n2 * m2)]过大
        // const n1 = this.getDecimalPlace(num1);
        // const n2 = this.getDecimalPlace(num2);
        // const m1 = Math.pow(10, n1);
        // const m2 = Math.pow(10, n2);
        // return (n1 * m1) * (n2 * m2) / (m1 * m2);

        // 方案2:
        // 用除法实现乘法,不会存在过大中间值
        let n1 = this.getDecimalPlace(num1);
        let n2 = this.getDecimalPlace(num2);

        let m = Math.pow(10, n2);
        num2 = m / this.toFixed(num2 * m);

        m = Math.pow(10, Math.max(n1, this.getDecimalPlace(num2)));
        m = this.toFixed(num1 * m) / this.toFixed(num2 * m);

        let n = Math.min(this.getDecimalPlace(m), n1 + n2);
        return this.toFixed(m, n);
    } else {
        return args.reduce((a, b) => this.mul(a, b))
    }
};
/**
 * 小数相除法
 * @param {Number} num1 浮点数
 * @param {Number} num2 浮点数
 * @returns {Number}
 */
exactMath.div = function (...args) {
    if (args.length === 2) {
        const num1 = args[0];
        const num2 = args[1];

        const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
        return this.toFixed(num1 * m) / this.toFixed(num2 * m);
    } else {
        return args.reduce((a, b) => this.div(a, b))
    }
};
/**
 * 取余
 * @param {Number} num1 浮点数
 * @param {Number} num2 浮点数
 * @returns {Number}
 */
exactMath.rem = function (...args) {
    if (args.length === 2) {
        const num1 = args[0];
        const num2 = args[1];
        const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));

        return this.toFixed(num1 * m) % this.toFixed(num2 * m) / m;
    } else {
        return args.reduce((a, b) => this.rem(a, b))
    }
};

/**
 * n次方,仅支持整数次方(正负都可以)
 * @param {Number} num 浮点数
 * @param {Number} n 整数
 */
exactMath.pow = function (num, n) {
    if (num == 0 && n == 0) {
        return 1;
    }
    if (num == 0 && n > 0) {
        return 0
    }
    if (num == 0 && n < 0) {
        return Infinity;
    }
    // num为负数,n为负小数,返回NaN
    if (num < 0 && n < 0 && Math.round(n) != n) {
        return NaN;
    }

    if (Math.round(n) != n) {
        throw new Error('n must be an integer');
    }

    let result = 1;

    if (n > 0) {
        for (let index = 0; index < n; index++) {
            result = this.mul(result, num);
        }
    } else if (n < 0) {
        for (let index = 0, len = Math.abs(n); index < len; index++) {
            result = this.div(result, num);
        }
    }

    return result;
};
/**
 * 开方运算【牛顿迭代法】
 * 
 * @param {Number} n
 * @returns 
 */
exactMath.sqrt = function (n) {
    if (n < 0) return NaN;
    if (n === 0) return 0;
    if (n === 1) return 1;
    let last = 0;
    let res = 1;
    let c = 50;
    while (res != last && --c >= 0) {
        last = res;
        res = this.div(this.add(res, this.div(n, res)), 2)
    }
    return res;

    // float InvSqrt(float x)
    // {
    //     float xhalf = 0.5f * x;
    //     int i = * (int *) & x; // get bits for floating VALUE 
    //     i = 0x5f375a86 - (i >> 1); // gives initial guess y0
    //     x = * (float *) & i; // convert bits BACK to float
    //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    //     x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    //     return 1 / x;
    // }
};

/****************************************************随机****************************************************/
function getSeed(seed) {
    if (isNaN(seed)) {
        seed = Math.floor(Math.random() * 233280);
    } else {
        seed = Math.floor(seed % 233280);
    }
    return seed;
}

let randomSeed = getSeed();

/**
 * 设置随机种子
 */
exactMath.setSeed = function (seed) {
    randomSeed = getSeed(seed);
};

/**
 * 随机
 */
exactMath.random = function () {
    randomSeed = (randomSeed * 9301 + 49297) % 233280;
    return randomSeed / 233280.0;
};

/**
 * 根据随机种子随机
 * @param {number} seed
 */
exactMath.randomBySeed = function (seed) {
    seed = getSeed(seed);
    seed = (seed * 9301 + 49297) % 233280;
    return seed / 233280.0;
};

/****************************************************角度弧度转换****************************************************/
/**
 * 弧度数转角度数
 * @param {Number} radians 浮点数
 * @returns {Numbe} 浮点数
 */
exactMath.radiansToDegrees = function (radians) {
    return this.div(radians, RAD);
};
/**
 * 角度数转弧度数
 * @param {Number} degrees 浮点数
 * @returns {Numbe} 浮点数
 */
exactMath.degreesToRadians = function (degrees) {
    return this.div(degrees, DEG);
};
/**
 * 将角度值转换到[0, 360)范围内
 * @param {Number} angle 浮点数
 * @returns {Number} 整数
 */
exactMath.get0To360Angle = function (angle) {
    if (angle === 0) {
        return 0;
    } else if (angle < 0) {
        return this.add(this.rem(angle, 360), 360);
    } else {
        return this.rem(angle, 360);
    }
};
/****************************************************三角函数****************************************************/
/**
 * 查表
 */
exactMath._sin = {};
exactMath._cos = {};
exactMath._tan = {};

/**
 * 3个三角函数,根据需求自行添加
 * 为了效率,应该尽量使用查表法
 * 表内查不到的,目前使用系统方法的结果并取前4位小数
 */
exactMath.sin = function (x) {
    if (this._sin.hasOwnProperty(x)) {
        return this._sin[x];
    }

    // if (x == 0) {
    //     return 0;
    // } else if (x == 90) {
    //     return 1;
    // }

    // let n = x, sum = 0, i = 1;
    // do {
    //     i++;
    //     sum = this.add(sum, n);
    //     // n = -n * x * x / (2 * i - 1) / (2 * i - 2);
    //     n = this.div(this.mul(-1, n, x, x), this.sub(this.mul(2, i), 1), this.sub(this.mul(2, i), 2));
    // } while (Math.abs(n) >= ACCURACY_SIN_ERROR);
    // return sum;

    return this.toFixed(Math.sin(x), 4);
};
exactMath.cos = function (x) {
    if (this._cos.hasOwnProperty(x)) {
        return this._cos[x];
    }

    return this.toFixed(Math.cos(x), 4);
};
exactMath.tan = function (x) {
    if (this._tan.hasOwnProperty(x)) {
        return this._tan[x];
    }

    return this.toFixed(Math.tan(x), 4);
};

使用

eMath.value(10).add(20.123).mul(2).sqrt().value
eMath.value(0.1).add(0.2).value

性能测试

// 在nodejs化境下与decimal对比(其它库我没用过,可以自己测试下)
var eMath = require('./eMath');
var decimal = require('./decimal');

var time = Date.now();
for (var i = 0; i < 1000000; i++) {
    eMath.value(1000.456789).add(200.123456).mul(100).div(2).add(100).sub(2).div(2).value;
}
var eTime = Date.now() - time;

time = Date.now();
for (var i = 0; i < 1000000; i++) {
    decimal(1000.456789).add(200.123456).mul(100).div(2).add(100).sub(2).div(2).toNumber();
}
var dTime = Date.now() - time;

console.log(`eMath: ${eTime}ms`);
console.log(`decimal: ${dTime}ms`);
console.log(`fast: ${(dTime - eTime) / dTime * 100}%`);

// 测了5次的结果
eMath: 690ms
decimal: 3212ms
fast: 78.51805728518057%

eMath: 671ms
decimal: 2881ms
fast: 76.7094758764318%

eMath: 654ms
decimal: 3014ms
fast: 78.30126078301261%

eMath: 706ms
decimal: 3071ms
fast: 77.01074568544448%

eMath: 691ms
decimal: 3243ms
fast: 78.69256860931236%

2020/06/06 更新:
【优化】去除所有字符串操作,性能提升30%

18赞

乘除法里都有pow,还有字符串操作,感觉性能堪忧

pow性能不错,只是乘法而已,sqrt消耗比较大。
字符串操作我也想避免,可是想知道小数位数的话我也没想到好办法,我已经尽可能的少使用字符串操作了,decimal里面用的比我这里还多。

测试了一下,将近慢了4倍,不过毫秒级的运算,影响不大吧

嗯,比普通使用肯定要慢,毕竟要转来转去。
具体有没有影响的话,还是要在实际项目中做分析。
我记得我之前好像测试过,少用sqrt方法,效率还是不错的,至少不会成为引起掉帧的主要原因。

同样的一次运算

eMath下

##decimal下


console.time不一定准确,但整体还是要比decimal快的

计算时间最好外面加一个for循环 100w次然后求一下平均时间

2020/06/06 更新:
【优化】去除所有字符串操作,性能提升30%

和原生写法性能差距大概多少呢?

拿上面的例子,原生写法只耗时2ms,eMath要600~700ms,300多倍:confused:
速度差多少还要取决小数位数的多少,开方等耗时操作的使用占比

赞,但暂时还不敢用,不确定源生提升的比例有多少,改了很多位置后万一速度反而慢了。
项目中使用了大量计算,持续关注

var a = gravity / 2;
var c = this.s[2];
var b = this.d[2] - a - c;

    var x = this.inter(this.s[0], this.d[0], t);
    var y = this.inter(this.s[1], this.d[1], t);
    var h = (a * t + b) * t + c;
    this.force_position(x, y); //此处设置在初始位置
    this.offsety = h;
    //目标的X - 起始X
    var dx = this.d[0] - this.s[0];
    //目标的Y - 起始Y
    var dy = this.d[1] - this.s[1];
    var dh = 2 * a * t + b;
    var r = Math.atan2(-(dy + 0) / 1.414, dx);
    var l = Math.sqrt(dx * dx + dy * dy + dh * dh);
    dx = dx / l;
    dy = dy / l;
    dh = dh / l;
    this.sprite_.angle = -(cc.misc.radiansToDegrees(cc.v2(dx, dy).signAngle(cc.v2(1, 0))));

请教一下,我运用了大量使用此类运算,如果使用emath,会不会有提升

速度不会提升的,这个库是要比原生慢的~~~~

感谢。
慢也是一个思路。mark一下。
希望能有更进一步的优化呢。

加法和乘法还好,除法还是会有小数的吧

@那些问效率有没有提升的小伙子
这个库,是帧同步的基础,目的不是拿来提升效率的吧。

3赞