faster printing of floats by divmod10() and others

robtillaart:
update: updated attachment print.cpp in post above as the default print behaviour for floats was broken (#decimals was incorrect)

this threads latest development slipped me by whilst you have been adding the SI output. nice suggestions and implimentation.

I see the overflow fix for the rounding overflow. will have to do mycode as well.

Fantastic + !
Now thinking about the space (before the SI postfix) and ie. E+08 vs. E+8 format..

ie. E+08 vs. E+8 format..

that is simple, apply this patch to the latest version

   case ENG:
    case SCI:
        n += write('E');
        if (exponent >= 0) n += write('+');
        if (exponent < 10) n += write('0');
        n += print(exponent);
        break;

update: patch does not work for negative exponents!!!

..and - rounding, for example:

0.2349 prints 234 m 
or
0.23429 prints 234.2 m

My understanding is the rounding with ENG/SI shall be:

0.2349 prints 235 m 
0.23429 prints 234.3 m

So for ENG/SI the digit right from the last printed digit shall be taken for a reference while rounding, I think..
Serial.print(0.23429, 4, ENG/SI);
0.23429

new patch ... (code increase)

    case ENG:
    case SCI:
        n += write('E');
        if (exponent >= 0) n += write('+');
        else 
        {
            exponent = -exponent;
            n += write('-');
        }
        if (exponent < 10) n += write('0');
        n += print(exponent);
        break;

PS: my above replay on rounding: it shall be applicable for SCI as well I think.
So always take for the rounding "reference" the digit right to the digit which will be printed last.

0.234293
SCI,3: 2.343E-01
ENG,4: 234.3E-3
SI,4: 234.3m

The rounding code is still based upon #decimals iso #total digits. And with the ENG notation the rounding factor depends indeed on the value of the last printed digit. But you have to determine that before printing.

Needs some thinking to make it robust for all 4 formats (first step is a notation dependent rounding and merge later)

For "number of digits" based formats (SCI/ENG/SI) the rule is following:

The reference for rounding is the digit right to the digit to be printed last.

So when printing with 4 digits, start with 4+1= 5 digits, make rounding based on the 5th digit, and then work with the first 4 digits as usual.

That's ok but one moment the 5th digit is O(E-1) the other time it is O(E-2) or O(E-3)

special test case is print(999.99, 4, ENG); which becomes 1000.0E+00 (wrong) or 1.000E+03 (right)
That's why determination of the exponent should be done after rounding, but to determine the rounding factor you need to know the exponent.

back with my changes to print.cpp to account for the actual ( real accurate digits the number can hold ) when in SCI / ENG mode, this should apply to the rounding you are seeing here ?

ie. if the data doesn't hold a value worth rounding then dont do it maybe ?

That's ok but one moment the 5th digit is O(E-1) the other time it is O(E-2) or O(E-3)

I did not study the code, but I would expect you go from raw mantissa (significand - there are all the digits available ), for n-digits-precision take first n+1, round, go back to n and so on. You do not need exponent to know for messing with digits, I think..

For 4 digits precision:
m:  999999   exp: x
take first 4+1 digits and round
m:  99999+5   exp:x
m: 100004 and  if 1st digit went from 9->1 x=x+1
take first 4 digits
m: 1000 exp: x+1
do conversion to ENG/SI

new BETA version of my print.cpp/.h

  • rounding SCI/ENG/SI

Print.cpp (19.9 KB)

Print.h (3.03 KB)

pito:

That's ok but one moment the 5th digit is O(E-1) the other time it is O(E-2) or O(E-3)

I did not study the code, but I would expect you go from raw mantissa (significand - there are all the digits available ), for n-digits-precision take first n+1, round, go back to n and so on. You do not need exponent to know for messing with digits, I think..

For 4 digits precision:

m:  999999   exp: x
take first 4+1 digits and round
m:  99999+5   exp:x
m: 100004 and  if 1st digit went from 9->1 x=x+1
take first 4 digits
m: 1000 exp: x+1
do conversion to ENG/SI

  • The raw mantissa is not a decimal value. It is binary, and the exponent is binary too (google IEEE754 wikipedia)
  • To change the first n digits of a float into an integer value I need to multiply the float by some number. To find that number is (almost) equivalent to finding the exponent.

I did not study the code,

please do ...

Hi Rob,

Thanks for your updates to print, I have been using bignumbers as a substitute.

Just wondering, is your fix going to become part of the default IDE?

Also, how hard would it be to implement longer 'standard' numbers in the IDE? I am interested in int128, for example. For that matter, it would be great if double was in fact a double float...

Constantin:
Hi Rob,

Thanks for your updates to print, I have been using bignumbers as a substitute.

Just wondering, is your fix going to become part of the default IDE?

Also, how hard would it be to implement longer 'standard' numbers in the IDE? I am interested in int128, for example. For that matter, it would be great if double was in fact a double float...

First the fixes/ideas here are not only mine as more people cooperated to get this far.

I do not expect it to become part of the default IDE soon as imho it is not tested enough yet. Furthermore there are two main features in it: faster printing and printing SCI/ENG/SI format.

  • The first has currently assembly in it, very fast but not portable to e.g. the DUE,
  • The SCI/ENG/SI formatting works only for floats at the moment. Maybe 3 extra formats is too much.
  • Support for printing long long; // maybe most stable part
    On the positive site the lib is backwards compatible so it can be used by the ones who want to.
    Finally Paul Stoffregen, Designer of the Teensy - Teensy USB Development Board -, will incorporate the work in a coming (not perse next) release. Mentioned above somewhere.

Implementing an IEEE754(?) 64bit double in the IDE is not trivial and would take quite some time.
Implementing a proprietary larger float is simpler, one can make a class that uses for example

Class Ardouble
{

private:
  bool s;    // sign, true => negative;
  unsigned long m; // 32 bits mantisse => 9 significant digits iso 32bit float 23 bits mantissse which has 6-7 significant digits.
  int exponent; // use power of 10 => -32767 .. +32767  (OK maybe a byte is enough ;)
};

Then you need to implement a math library and conversion from and to existing base types (at least float), not very difficult but not trivial either. quite some work.
Start with +* -/% and boolean operators like < > == and !=
If that works then continue with functions like sin() cos() exp() ln() pow() sqrt() trunc() floor() ceil() etc.

Implementing an int128 class is probably easier but it will be very slow. Have you ever worked with the LONG LONG ?

Hi Rob,

Yeah, I saw the assembly code and my heart sank - can't use it in the Teensy! However, I am psyched to hear that Paul may be incorporating it into the Teensy IDE... makes sense too given how many bits a 32-bit processor can handle/accumulate/etc.

As for double, I am curious why it was implemented at all if it simply defaults to float precision. I would have simply left it out.

And yes, I worked with long long until I hit a wall of sorts... one of my projects involved a least squares solver for 16 Bit ADC error minimization. Worked like a charm in Excel but numbers with 25 digits couldn't be handled by a 2^64 capable variable (i.e. can't handle anything bigger than 1.84e+19). Hence the use of the BigNumbers library (Thank you, Mr. Gammon!).

Given my knowledge re: libraries, etc. I'll simply continue using BigNumbers then for these sorts of jobs. All the more advisable since Mr. Gammon and Stoffregen have implemented BigNumbers on the Teensy as well.

As for double, I am curious why it was implemented at all if it simply defaults to float precision. I would have simply left it out.

I think it is mandatory to implement double to keep the compiler happy, and that the implementation is platform dependent.

going on from my changes to printNumber ( in print.cpp ) where I suggested an extra parameter to being the leading zeroes ( positive for x number of them, 0 or negative to print none )

it also gains a nice addition of printing HEX and BIN numbers for free.

size_t Print::printNumber(uint32_t num, uint8_t base, uint8_t leading_zeros) {
  char buf[33]; // Assumes 8-bit chars plus zero byte. was -> "8 * sizeof(int32_t) + 1"
  char *str = &buf[sizeof(buf) - 1];
  *str = '\0';
......
  int8_t extra_digits = leading_zeros;

......
......
......

  // have we got some leading zero's to also print ?
  for ( ; extra_digits > 0; extra_digits-- ) *--str = '0';

  return write(str);
}

and then in print.h

class Print
{
private:
......
    size_t printNumber(uint32_t, uint8_t, uint8_t);

public:
......
......
    size_t print(uint8_t num, uint8_t base)     { return (base == HEX) ? printNumber(num, HEX, 2) : (base == BIN) ? printNumber(num, BIN, 8) : printNumber(num, base, NO_LEADING_ZERO); }
    size_t print(int16_t num, uint8_t base)     { return print((int32_t)num, base); }
    size_t print(uint16_t num, uint8_t base)    { return (base == HEX) ? printNumber(num, HEX, 4) : (base == BIN) ? printNumber(num, BIN, 16) : printNumber(num, base, NO_LEADING_ZERO); }
    size_t print(int32_t num, uint8_t base)     { return (base == DEC) ? print(num) : printNumber(num, base, NO_LEADING_ZERO); }
    size_t print(uint32_t num, uint8_t base)    { return (base == HEX) ? printNumber(num, HEX, 8) : (base == BIN) ? printNumber(num, BIN, 32) : printNumber(num, base, NO_LEADING_ZERO); }
......
......

lots of code missed by the "....." but the listed codes gives the idear of what to patch / add.

Rob, your suggested way of adding the leading zeroes ( when doing the decimal part of output of course needs to be changed to the way I had used, but its useful for the freebie gain on HEX and BIN output I think.

would fix this call as well

darryl, if I'm reading your code right you're changing the behaviour of Print.print(1, HEX) to output "01" instead of "1", right? I don't expect that change to be ever accepted into the Arduino codebase - doing so causes sketches that rely on the old behaviour (for example because they print the leading zero manually) to break.

To do this properly, I think the leading zero printing must be made optional, defaulting to not printing them. An easy way is to just add a "min_digits" argument to the print functions, which defaults to 0?