WG21 N1567=04-0007 CRITIQUE OF WG14/N1016 DECIMAL FLOATING-POINT ARITHMETIC P.J. Plauger Dinkumware, Ltd. pjp@dinkumware.com Both WG14 and WG21 have accepted WG14/N1016 as the basis for parallel non-normative Technical Reports, adding decimal floating-point arithmetic to C and C++. The decimal formats are based on work done at IBM plus current standardization work within IEEE -- a revision of IEEE 754, the widely adopted standard for binary floating-point arithmetic. The revised standard IEEE 754R will describe both binary and decimal formats. N1016 proposes adding three more basic types to C (and C++), for decimal floating-point to coexist alongside whatever an implementation currently uses for float, double, and long double. That also involves: -- adding literal formats for the new types, such as 1.0DF -- adding promotion and conversion rules between the new types and existing basic types -- adding macros to <fenv.h> to describe new rounding modes and exceptions -- adding a new header <decfloat.h> to describe properties of the new types a la <float.h> -- adding macros to <math.h> to describe huge values and NaN for the new types -- adding versions of all the math functions in <math.h> for the new types -- adding half a dozen new functions to <math.h> to perform operations particular to decimal floating-point on the new types -- adding new conversion specifications, such as %GLD, to the formatted input/output conversions in <stdio.h> and <wchar.h> -- adding strto* functions to <stdlib.h> for the new types -- adding wcsto* functions to <wchar.h> for the new types -- adding the relevant macros to <tgmath.h> for the new functions added to <math.h> There is no provision for new complex types in C based on the decimal floating-point types. Adding three new types is clearly a major change to C. C++ could avoid the proliferation of names by overloading existing names, but it shares all the other problems. It is not at all clear to me that much need exists for having two sets of floating-point types in the same program. It is certainly not clear to me that whatever need exists is worth the high cost of adding all these types. Lower-cost alternatives exist. Probably the cheapest is simply to define a binding to IEEE 754R, much like the current C Annex F for IEC 60559 (the international version of IEEE 754). If we do this, most of the above list evaporates. But I believe we should still add a few items to the C library, and the obvious analogs to C++, independent of whether the binding is provided by the compiler. These involve: -- adding macros to <fenv.h> to describe *some optional* new rounding modes but *no* new exceptions -- adding half a dozen new functions to <math.h> to perform operations demonstrably useful for decimal floating-point but not unreasonable even for other floating-point formats -- adding the relevant macros to <tgmath.h> for just the half a dozen new functions added to <math.h> An implementation that chooses this option would also get C complex decimal in the bargain. Defining a binding does not oblige compiler vendors to switch to decimal floating-point, however. For programmers to get access to this new technology, they would have to wait for a vendor to feel motivated to make major changes to both compiler and library. And it is not just the vendor who pays a price: -- The major cost for the unconcerned user is a slight reduction in average precision, due mainly to the greater "wobble" inherent in decimal vs. binary format. The major payoff is fewer surprises of the form (10.0 * 0.1 != 1.0), and perhaps faster floating-point input/output. -- A bigger cost falls on those programmers who need to convert often between decimal floating-point and existing formats. These could be a rare and special breed, but there might also be a distributed cost in performance and complexity among users who have to access databases that store floating-point results in non-decimal encoded formats. Experts have to write the converters; many non-experts might have to use them. So if all we do is define a binding, it could take a long time for decimal floating-point to appear in the marketplace. But a reasonably cheap alternative can mitigate this problem. Simply define a way to add decimal floating-point as a pure bolt-on to C and C++ -- a library-only package that can work with exsiting C and C++ compilers. For C, this means adding one or more new headers that define three structured types and a slew of functions for manipulating them. For C++, the solution can look much like the existing standard header <complex> -- a template class plus operators and functions that manipulate it, with the three IEEE 754R decimal formats as explicit instantiations. The major compromise in a bolt-on solution is the weaker integration of decimal floating-point with the rest of the language and library. C suffers most because it doesn't permit operator overloading for user-defined types. (C++ seems to be doing just fine with complex as a library-defined type.) The payoff is a much greater chance that vendors will supply implementations sooner rather than later. I believe the best thing is to do both of these lightweight things, instead of adding three more floating-point types to the C and C++ languages. Implementing a TR of this form assures programmers that they can reap the benefits of decimal floating-point one way or the other. And such a TR provides a road map for how best to supply decimal floating-point for both the short and long term. A FEW DETAILED CRITICISMS OF N1016 The header <decfloat.h> should not define names that differ arbitrarily from existing names in <float.h> (e.g. DEC32_COEFF_DIG). ----- The rounding modes in <fenv.h> have even more confusing differences in naming. In C99, for example, "down" means "toward -infinity", while in N1016 it means "toward zero". Here's a Rosetta Stone: N1016 C99 (meaning) FE_DEC_ROUND_DOWN FE_TOWARDZERO (toward zero) FE_DEC_ROUND_HALF_EVEN FE_TONEAREST (ties to even) FE_DEC_ROUND_CEILING FE_UPWARD (toward +inf) FE_DEC_ROUND_FLOOR FE_DOWNWARD (toward -inf) FE_DEC_ROUND_HALF_UP (ties away from zero) FE_DEC_ROUND_HALF_DOWN (ties toward zero) FE_DEC_ROUND_UP (away from zero) Only the last two modes are optional in N1016. ----- Similarly, for floating-point exceptions, we have: N1016 C99 FE_DEC_DIVISION_BY_ZERO FE_DIVBYZERO FE_DEC_INVALID_OPERATION FE_INVALID FE_DEC_INEXACT FE_INEXACT FE_DEC_OVERFLOW FE_OVERFLOW FE_DEC_UNDERFLOW FE_UNDERFLOW There's no good reason for the differences in the first two lines. ----- Many of the functions cited in N1016 are *not* present in the latest draft of IEEE 754R, as advertised. I had to hunt down specifics at: http://www2.hursley.ibm.com/decimal/decbits.pdf http://www2.hursley.ibm.com/decimal/decarith.pdf ----- There's no reason for <math.h> to have HUGE_VALF, etc. followed by DEC32_HUGE, etc. Once again, the names should not differ arbitrarily. It's also not clear why there should be a DEC_NAN and not a DEC_INF (or DEC_INFINITY). Either both are easily generated as inline expressions (0D/0D, 1D/0D, etc.) or neother is. I favor defining both as macros (perhaps involving compiler magic). ----- N1016 calls for the interesting function: T divide_integerxx(T x, T y); (where xx stands for d32, d64, or d128). This generates an integer quotient only if it's exactly representable. But N1016 doesn't require the corresponding remainder function. I suggest loosely following the pattern of remquo and adding an optional pointer to where to return the remainder: T divide_integerxx(T x, T y, T *prem); Or we could follow the pattern of remquo even closer and replace this function with remainder_integerxx that returns the quotient on the side. ----- N1016 calls for the function: T remainder_nearxx(T x, T y); This has the same specification as the C99 remainder function. It should share the same root name (e.g. remainderd32, after remainderf). ----- N1016 calls for the function: T round_to_integerxx(T x, T y); This has the same specification as the C99 rint function. It should share the same root name. ----- N1016 calls for the interesting function: T normalizexx(T x); This shifts the coefficient right until the least-significant decimal digit is nonzero, or it changes a zero value to canonical form. It's not clear what should happen if such a shift would cause an overflow, but that behavior must be specified. (It's also not clear what the purpose of this function is in the best of circumstances, but maybe I haven't read and played enough to understand.) Finally, this function does exactly the opposite of what "normalize" has meant as a term of art in floating-point for many decades. The name suggests that the coefficient is shifted *left* until the *most-significant" decimal digit is nonzero. (And I can even think of uses for that operation.) Either the spec should change or the name. ----- N1016 calls for the function: bool check_quantumxx(double x, double y); This returns true only if x and y have the same exponent. There is no spec for this function in N1016, but it is clearly the same as the function same_quantum in the defining document from IBM. I see no good reason for changing the name. ----- N1016 calls for the interesting function: T quantizexx(T x, T y); This changes x, as need be, to have the same exponent as y. It is, in effect, a "round to N decimal places" function. In conjunction with the rounding mode, it provides the much-touted "proper" decimal rounding rules to match various government rules and commercial practices. It's not entirely clear to me (yet) why a similar function couldn't reap much the same benefit even when used with binary floating-point. (I hasten to add that there are still other good reasons for using decimal floating-point instead.) In any event, I believe that it can and should be generalized so that its application to binary floating-point makes equal sense. Unfortunately, the function in its current form gets its parameter N from the *decimal exponent* of y. That supports cute notation such as: price = quantized32(price * (1DF + tax_rate), 1.00DF); assuming that you find "1.00DF" more revealing than "2". But it thus relies heavily on the ability to write literals (or generate values) with a known decimal exponent. An earlier version evidently required/permitted you to write the number of decimal digits instead. I've long used an internal library function that truncates binary floating-point values to a specified number of binary places (plus or minus). I could see a real benefit in having both binary and decimal versions of "quantize" that apply to floating-point values of either base. But this particular form does not generalize at all well. ----- N1016 does *not* call for several other functions that suggest themselves. These include decimal equivalents of frexp and ldexp, and possibly an exp10. I know from past experience that some of these are highly useful in writing math functions; but I need more experience writing IEEE 754R decimal floating-point math functions before I can make really informed recommendations. Nevertheless, the absence of such functions, and the lapses in the functions presented in N1016, suggests to me the need for more experience in using the decimal stuff from IEEE 754R as a general floating-point data type before we freeze a TR of any sort, for either C or C++.