That'd be a pretty serious performance hit to take.  Do you know of any
language where floating point comparisons work like that?  Certainly in
both C and Perl you will never get 0.8 to equal 0.7+0.1 exactly.

Just because nobody else does it is of course not reason enough not to do
it, but doing 2 sprintf's for every floating point comparison makes me
cringe.

-Rasmus

On Tue, 19 Mar 2002, George Whiffen wrote:

> Hi Folks,
>
> 0.8 == 0.7 + 0.1 or does it?
>
> I know I've brought this up before, but it's still driving me nuts, and
> I still don't know how to explain it to a novice, so rather than go on
> ranting I thought I'd try a fix.
>
> I'm no C programmer, and what I know about php source can be written on
> the back of a very small envelope.  But at least you can laugh at my
> appalling code, and, hopefully, explain to me why it can't go in the
> next release ;).
>
> Summary
> =======
> The fix is based on a string convert and comparison. It seems to work,
> is not too serious from a performance point of view, and only involves
> two sources, four functions and a 100 lines or so of code.  It does not
> create cumulative rounding errors, is easily controlled via an existing
> ini variable i.e. "precision" and improves the consistency of php's use
> of precision.
>
> The Bugs/Undesirable Features
> =====================
> 0.8 == 0.7 + 0.1 evaluates to false
> (int) (8.2 - 0.2) evaluates 7
> intval(8.2 - 0.2) evaluates to 7
> floor(10 * (0.7 + 0.1)) evaluates to 7
> ceil (10 * (-0.7 + -0.1) evaluates to -7
>
> Basis of the Fix
> ==========
> In general these would all evaluate correctly if they were evaluated to
> the precision specified in php.ini (typically 14 for IEEE 64bit).
>
> The fix replaces the current equality test on doubles, (a - b == 0),
> with  a string compare to 'precision' decimal places e.g.
> sprintf("%.14G",a) == sprintf("%.14G",b).  This is a modification to the
> doubles section of the Zend/zend_operators.c compare_function, which
> ultimately handles all comparisons, ==, !=, >=, >, <, <=.
>
> Floor, ceil, (int), intval(), are fixed with an equality check of their
> integer result to one above or below it, (as  appropriate) via the
> modified compare_function.
>
> e.g. floor becomes
> if  a == floor(a) + 1
> return floor(a) + 1;
> else
> return floor(a)
>
> Apart from some performance tweaks, that's about it i.e.
> Zend/zend_operators.c:compare_function
> Zend/zend_operators.c:convert_to_long_base
> ext/standard/math.c:ceil
> ext/standard/math.c:floor
>
> Testing
> =======
> The fixes seem to work. However, they have only been tested on a 4.1.2
> source under Linux 2.4.8-26mdk. They solve the problems listed above
> and  do as well as a basic cgi build of 4.1.2 on run-tests.php (i.e.
> they pass everything except pow.phpt, pear_registry.phpt and 029.phpt).
> They also seem to behave identically to an unfixed version if
> 'precision' is set high enough e.g. set_ini('precision',18).
>
>
> Backward Compatibility
> ================
> I've tried and failed to come up with a realistic scenario where this
> fix compromises existing user code.
>
> The main reason is that  string, printf, sprintf already force the
> precision set in php.ini.  This means it quite difficult for someone to
> have exploited the fact that compare_function does not.  To hit problems
> with the fix they would have to have first  decided they care about the
> digits beyond 'precision',  but do not care enough to use the bc
> library.  They then have to gone to some trouble to get hold of those
> extra digits. They could not, for instance, easily get them into a page
> or database without a conversion to string automatically removing the
> extra digits along the way.
>
> In contrast to a "round to precision after each floating point
> operation" approach, (which would be a nightmare), these fixes should
> not create any new issues with cumulative rounding errors.  All the
> changed functions already return booleans, ints, or integer-rounded
> floats.
>
> In any case, everything can easily be reverted to the old functionality
> at execution time simply by  increasing the value of precision e.g.
> set_ini('precision',18).
>
>
> Performance
> ===========
> Only doubles are effected.  Long comparisons, such as integer for-loops
> (for($i=0;$i<$sizeof($aray);++$i) etc.), are unchanged and run just as
> fast.
>
> The effect on double comparisons is not negligible.  sprintf's are
> relatively expensive in terms of performance and adding the overhead of
> two sprintfs to every single double compare would have been nasty.
>
> To minimise this the new compare_function code first of all checks that
> the operands are not already equal and are "close enough" that a string
> compare is necessary before forcing the sprintfs. The test is for (a-b)
> != 0 && ((a-b)/a) < 1e-(precision-1) e.g. (0.2 - 0.1) != 0 && ((0.2 -
> 0.1)/0.1) < 1e-13.
>
> Even when the operands are close and this test is the only overhead,
> this still means an  extra floating point division which accounts for
> nearly all the performance degradation.  compare_function is so fast
> already, that this extra division seems to make comparisons of non-equal
> doubles about twice as slow.
>
> Fortunately this is pretty small in absolute terms and when compared to
> other simple comparisons. A double comparison using the fix e.g. 0.1 ==
> 0.2, is still no slower than a single character non-numeric string
> comparison e.g. 'a' == 'b'.  They remain much faster than a numeric
> string compare e.g. '0.1' == '0.2'.
>
> When operands are close e.g. 1.2345678901234e123 == 1.2345678901233e123
> then the performance hit is much bigger as the two sprintfs and a string
> compare are required.  Even then it is still faster than the old
> workaround of casting to string before the compare i.e. (string) 0.1 ==
> (string) 0.2.
>
> Further Optimisations
> =====================
>
> The code can definitely be optimised further.
>
> The main slowdown on the ordinary, non-close, comparisons comes from the
> floating point divide which is needed to make sure that it is the
> "relative" difference not the absolute difference which is compared to
> the precision.  If the precision were converted to binary and adjusted
> by the value of the double's exponent it should be possible to avoid the
> floating point division.  But this is significantly more complicated, or
> rather I haven't worked out how to do it!
>
> The sprintf/string compare could also be improved.   For example,
> sprintf("%a") seems to be about twice as fast as sprintf("%G").
> Unfortunately %a can return un-normalized formats which would need
> tweaking to stop them failing the string comparison.
>
> There are also significant performance improvements possible in the
> floor, ceil, intval functions by not using compare_function but instead
> doing an in-line comparison.  Since the results of these functions are
> themselves integers their differences can be compared directly to
> 1e(-precision) without the floating point divide, (provided of course
> they are less than 1e13 themselves).
>
>
> Many thanks for taking the time to consider this. Any feedback will be
> much appreciated.
>
>
> George
>
> Changes to Zend/zend_operators.c:
>
> compare_function
> ============
>
> OLD CODE:
> --------------
>  if ((op1->type == IS_DOUBLE || op1->type == IS_LONG)
>   && (op2->type == IS_DOUBLE || op2->type == IS_LONG)) {
>   result->value.dval = (op1->type == IS_LONG ? (double) op1->value.lval
> : op1->value.dval) - (op2->type == IS_LONG ? (double) op2->value.lval :
> op2->value.dval);
>    result->value.lval = ZEND_NORMALIZE_BOOL(result->value.dval);
>    result->type = IS_LONG;
>    return SUCCESS;
>  }
>
> NEW CODE:
> --------------
> ...
>         double dval1, dval2;
>         char   *sval1, *sval2;
>         long   slen1, slen2;
> ...
>  if ((op1->type == IS_DOUBLE || op1->type == IS_LONG)
>   && (op2->type == IS_DOUBLE || op2->type == IS_LONG)) {
>
>           if (op1->type == IS_DOUBLE) {
>             dval1 = op1->value.dval;
>           } else {
>             dval1 = (double) op1->value.lval;
>           }
>           if (op2->type == IS_DOUBLE) {
>             dval2 = op2->value.dval;
>           } else {
>             dval2 = (double) op2->value.lval;
>           }
>           result->value.dval = dval1 - dval2;
>
>           if (result->value.dval != 0 && fabs((dval1 - dval2)/(dval1 ==
> 0 ? 1 : dval1)) < pow(10.0,0 - (EG(precision) -1))) {
>        sval1 = (char *) emalloc(MAX_LENGTH_OF_DOUBLE + EG(precision) +
> 1);
>        slen1 = sprintf(sval1, "%.*G", (int) EG(precision),dval1);  /*
> SAFE */
>        sval2 = (char *) emalloc(MAX_LENGTH_OF_DOUBLE + EG(precision) +
> 1);
>        slen2 = sprintf(sval2, "%.*G", (int) EG(precision),dval2);  /*
> SAFE */
>        if (0 == zend_binary_strcmp(sval1,slen1,sval2,slen2))
>       {
>          result->value.dval = 0;
>        }
>        efree(sval1);
>        efree(sval2);
>      }
>      result->value.lval = ZEND_NORMALIZE_BOOL(result->value.dval);
>      result->type = IS_LONG;
>      return SUCCESS;
>  }
>
>
> convert_to_long_base
> ===============
>
> OLD CODE:
> --------------
> ...
>   case IS_DOUBLE:
>    DVAL_TO_LVAL(op->value.dval, op->value.lval);
>    break;
> ...
>
> NEW CODE:
> --------------
> ...
>  zval *op1, *op2, *result;
> ...
>   case IS_DOUBLE:
>     MAKE_STD_ZVAL(op1);
>     MAKE_STD_ZVAL(op2);
>     MAKE_STD_ZVAL(result);
>     op1->type = IS_DOUBLE;
>     op2->type = IS_LONG;
>                   op1->value.dval = op->value.dval;
>     DVAL_TO_LVAL(op->value.dval, op->value.lval);
>                   if (op1->value.dval >= 0)
>                    {
>                       op2->value.lval = op->value.lval + 1;
>                     } else {
>                       op2->value.lval = op->value.lval - 1;
>                     }
>     compare_function(result, op1, op2  TSRMLS_CC);
>     if (result->value.lval == 0) {
>       op->value.lval = op2->value.lval;
>     }
>     zval_dtor(result);
>     zval_dtor(op1);
>     zval_dtor(op2);
>    break;
>
> The changes to floor and ceil in ext/standard/math.c are very similar to
> the convert_to_long_base change.
>


-- 
PHP Development Mailing List <http://www.php.net/>
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to