Fast arbitrary-precision logarithms with bcmath

隐身守侯 提交于 2019-12-12 11:40:12

问题


Here's what I've got

function bcln($n, $scale=10) {
    $iscale = $scale+3;
    $result = '0.0';
    $i = 0;

    do {
        $pow = (1 + (2 * $i++));
        $mul = bcdiv('1', $pow, $iscale);
        $fraction = bcmul($mul, bcpow(bcsub($n, '1', $iscale) / bcadd($n, '1.0', $iscale), $pow, $iscale), $iscale);
        $lastResult = $result;
        $result = bcadd($fraction, $result, $iscale);
    } while($result !== $lastResult);

    return bcmul('2', $result, $scale);
}

But this takes 5.7 seconds to run bcln(100) (natural log of 100, 10 decimal places). Furthermore, it's not always accurate for more decimal places. Is there a better algorithm?

For that specific run, it takes 573 iterations to settle on the result.


回答1:


Do you require an arbitrary-length string as an answer? Or do you require arbitrary precision, or arbitrary exponent size? Or… would a double-precision floating-point answer (return-value) suffice; given that we're "only" working with the logarithm of a number of "arbitrary" size?

Double precision floating point numbers have an 11-bit signed exponent: therefore, if your big number string has a length of ≤1022 bits ≈ 307 decimal digits (so string length of 306 characters including the decimal point), you are safe! More accurately, you should be safe if the absolute value of the resulting decimal exponent is ≤307. Do you need bigger exponents than that? (I suppose in other words: are you working with real-world numbers or theoretical/ pure mathematics?)

Why not just use some string processing, along with some simple floating-point log arithmetic? This should be very fast, for any real-world numbers…

function bclog10($n){
    //←Might need to implement some validation logic here!
    $pos=strpos($n,'.');
    if($pos===false){
        $dec_frac='.'.substr($n,0,15);$pos=strlen($n);
    }else{  $dec_frac='.'.substr(substr($n,0,$pos).substr($n,$pos+1),0,15);
    }
    return log10((float)$dec_frac)+(float)$pos;
}

You can convert the base using some well-known log arithmetic:

function bclogn($n,$base=M_E){//$base should be float: default is e
    return bclog10($n)*log(10)/log($base);
}

I have tested these functions and they work for me, for the examples I supplied; giving exactly the same answers as the Windows 10 calculator, up to the limits of double-precision arithmetic as used by PHP.

If you actually need more than 15 digits of precision and more than 307 for the decimal exponent, you might be able to implement your own "BigFloat" class object, and somehow build its methods out of the standard built-in floating-point functions using a divide-and-conquer approach! Then perhaps, we might use that as the basis of an arbitrary-precision floating-point logarithm algorithm, by combining this with the functions/techniques described above. You might want to consider consulting with the people at math.stackexchange.com, to find out more about whether this could be a feasible approach.

MAJOR EDIT: 2nd attempt…

function bclog10($n){//By Matthew Slyman @aaabit.com
    $m=array();// ↓ Validation, matching/processing regex…
    preg_match('/^(-)?0*([1-9][0-9]*)?(\.(0*))?([1-9][0-9]*)?([Ee](-)?0*([1-9][0-9]*))?$/',$n,$m);
    if(!isset($m[1])){throw new \Exception('Argument: not decimal number string!');}
    $sgn=$m[1];if($sgn==='-'){throw new \Exception('Cannot compute: log(<⁺0)!');}
    $abs=$m[2];$pos=strlen($abs);
    if(isset($m[4])){$fre=$m[4];}else{$fre='';}$neg=strlen($fre);
    if(isset($m[5])){$frc=$m[5];}else{$frc='';}
    if(isset($m[7])){$esgn=$m[7]==='-'?-1:1;}else{$esgn=1;}
    if(isset($m[8])){$eexp=$m[8];}else{$eexp=0;}
    if($pos===0){
        $dec_frac='.'.substr($frc,0,15);$pos=-1*$neg;
    }else{  $dec_frac='.'.substr($abs.$fre.$frc,0,15);
    }
    return log10((float)$dec_frac)+(float)$pos+($esgn*$eexp);
}


来源:https://stackoverflow.com/questions/24945193/fast-arbitrary-precision-logarithms-with-bcmath

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!