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