Skip to content

py/formatfloat: Improve accuracy of float formatting code. #17444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

yoctopuce
Copy link
Contributor

@yoctopuce yoctopuce commented Jun 6, 2025

Summary

Following discussions in PR #16666, this pull request updates the float formatting code to reduce the repr reversibility error, i.e. the percentage of valid floating point numbers that do not parse back to the same number when formatted by repr.

The baseline before this commit is an error rate of ~46%, when using double-precision floats.

This new code is available in two flavors, based on a preprocessor definition:

  • In the simplest version, it reduces the error down to ~40%, using an integer representation of the decimal mantissa rather than working on floats. It is also slightly faster, and improves the rounding in some conditions.
  • In the most complete version, it reduces the error down to ~5%. This extra code works by iterative refinement, and makes the code slightly slower than CPython when tested on ports/unix.

Testing

The new formatting code was tested for reversibility using the code provided by Damien in PR #16666
A variant using formats {:.7g}, {:.8g} and {:.9g} was used for single-precision testing.

Compatibility with CPython on the various float formats was tested by comparing the output using the following code:

for mant in [34567, 76543]:
    for exp in range(-16, 16):
        print("Next number: %de%d" % (mant, exp))
        num = mant * (10.0**exp)
        for mode in ['e', 'f', 'g']:
            maxprec = 16
            # MicroPython has a length limit in objfloat.c
            if mode == 'f' and 6 + exp + maxprec > 31:
                maxprec = 31 - 6 - exp
            for prec in range(1, maxprec):
                fmt = "%." + str(prec) + mode
                print("%5s: " % fmt, fmt % num)

The integration tests have also found some corner cases in the new code which have been fixed.
For single-precision floats, some test cases had to be adapted:

  • float_format_ints is tapping into an ill-defined partial digit of the mantissa (the 10th), which is not available in single-precision floats with the new code due to integer limitations. So the display range has been updated accordingly.
  • similarly, float_struct_e uses a 15-digit representation which is meaningless on single-precision floats. A separate version for double-precision has been made instead
  • in float_format_ints, there is one test case specific to single-precision floats which verifies that the largest possible mantissa value 16777215 can be used to store that exact number and retrieve it as-is. Unfortunately the rounding in the simplest version of the new algorithm makes it display as a slightly different number. This would cause the CI test to fail on single-precision floats when the improved algorithm is not enabled.

Trade-offs and Alternatives

It is unclear at that point if the simplest version of this improvement is worth the change:

  • going from 46% error to 40% error in double precision is not a big improvement.
  • there is no improvement for single-precision
  • the new code is only marginally faster

The full version of the enhancement makes much more difference in terms of precision, both for double-precision and single-precision floats, but it causes about 20% overhead on conversion time, and makes the code a bit bigger

Looking forward to reading your feedback...

Edit 1: See #17444 (comment) for updates on accuracy results
Edit 2: Updated values in #17444 (comment)

Copy link

github-actions bot commented Jun 6, 2025

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +184 +0.022% standard
      stm32:  +244 +0.062% PYBV10
     mimxrt:  +448 +0.120% TEENSY40
        rp2:  +240 +0.026% RPI_PICO_W
       samd:  +280 +0.104% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:  +214 +0.047% VIRT_RV32

Copy link

codecov bot commented Jun 6, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.56%. Comparing base (1b0cdc0) to head (7034fad).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #17444   +/-   ##
=======================================
  Coverage   98.56%   98.56%           
=======================================
  Files         169      169           
  Lines       21946    21948    +2     
=======================================
+ Hits        21632    21634    +2     
  Misses        314      314           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 6efb905 to ef61c67 Compare June 6, 2025 18:11
@yoctopuce
Copy link
Contributor Author

Additional results after a few more experiments in double-precision :

  • with the simple method, %17g provides the fastest repr value with ~40% error
  • with the elaborate method enabled, we can bring repr error down to 0.5% as follows:
    1. try first %17g, and stop if a perfect match is found (90% of the cases)
    2. otherwise fallback to %18g (7% of the cases)
    3. otherwise fallback to %19g (remaining cases)
      The overall cost is only 20% more than the fastest repr that had 40% error.

@dpgeorge
Copy link
Member

dpgeorge commented Jun 8, 2025

Thanks for this. I will study it in detail.

I also found the corresponding CPython issue and long discussion about this topic: python/cpython#45921 . There's a lot of useful information there. It looks like this is a hard thing to get right.

@yoctopuce
Copy link
Contributor Author

I also found the corresponding CPython issue and long discussion about this topic: python/cpython#45921 . There's a lot of useful information there. It looks like this is a hard thing to get right.

I read that thread, it is indeed an interesting input. But I am afraid that we will not be able to use much of it due to ressource constraints in MicroPython.

@yoctopuce
Copy link
Contributor Author

  • with the elaborate method enabled, we can bring repr error down to 0.5% as follows:

Out of curiosity, I have been investigating a bit more...

Half of these remaining errors are caused by {:g} falling back to {:f} for small negative exponents and therefore loosing significant digits in the mantissa. If we run the test using {:e}, the reprerror goes down to 0.25%. So I am looking at changing the code for {:f} to insert leading zeroes without integrating them in the mantissa. This should properly fix the issue and improve the accuracy of both {:f} and {:g}.

About 20% of the remaining errors are caused by large negative exponents (exp < -255). There is probably something we could improve there as well, but I am not sure it is a big concern for MicroPython if such seldom-used numbers have accuracy problems in repr.

I am also looking at cleaning a bit the code, to reduce code footprint. While doing this, one things that I noticed is that the function includes lots of checks on buf_size and buf_remaining at various places, but MicroPython always uses a 32 bytes buffer (or 16 bytes buffer for single-precision floats). Don't you think it would make sense to document this function as requiring a 32 bytes output buffer, adding an assert(buf_size >= 32) at the beginning and simplifying these checks ?

@dpgeorge
Copy link
Member

Don't you think it would make sense to document this function as requiring a 32 bytes output buffer, adding an assert(buf_size >= 32) at the beginning and simplifying these checks ?

Yes, that makes a lot of sense.

@dpgeorge
Copy link
Member

Related: #6024 has been around for a while and attempt to improve the other side of this, ie parsing of floats, using higher precision ints.

@yoctopuce
Copy link
Contributor Author

Thanks, I will look at it as well. I have a big update to submit for this PR, which I believe makes things look much better.
But I just found an accuracy issue in the float parse code for very small numbers, so I need to fix it first and check the impact on results...

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from fb265b8 to 9652e45 Compare June 13, 2025 14:11
@yoctopuce
Copy link
Contributor Author

I have just force-pushed my new code.
In addition to previous test code and to running the CI tests, I have been testing using the following routine:

import array, math, time, binascii

seed = 42


def pseudo_randouble():
    global seed
    ddef = []
    for _ in range(8):
        ddef.append(seed & 0xFF)
        seed = binascii.crc32(b'\0', seed)
    arr = array.array('d', bytes(ddef))
    return ddef, arr[0]


# The largest errors come from seldom used very small numbers, near the
# limit of the representation. So we keep them out of this test to keep
# the max relative error display useful.
if float('1e-100') == 0:
    # single-precision
    min_expo = -96  # i.e. not smaller than 1.0e-29
    # Expected results:
    # HIGH_QUALITY_REPR=1: 99.71% exact conversions, relative error < 1e-7
    # HIGH_QUALITY_REPR=0: 94.89% exact conversions, relative error < 1e-6
else:
    # double-precision
    min_expo = -845  # i.e. not smaller than 1.0e-254
    # Expected results:
    # HIGH_QUALITY_REPR=1: 99.83% exact conversions, relative error < 2.7e-16
    # HIGH_QUALITY_REPR=0: 64.01% exact conversions, relative error < 1.1e-15

ttime = 0
stats = 0
N = 10000000
max_err = 0
for _ in range(N):
    (ddef, f) = pseudo_randouble()
    while f == math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo:
        (ddef, f) = pseudo_randouble()

    start = time.time_ns()
    str_f = repr(f)
    ttime += time.time_ns() - start
    f2 = float(str_f)
    if f2 == f:
        stats += 1
    else:
        error = abs(f2 - f) / f
        if max_err < error:
            max_err = error
            print("{:.19e}: repr='{:s}' err={:.4e}".format(f, str_f, error))

print("{:d} values converted in {:d} [ms]".format(N, int(ttime / 1000000)))
print("{:.2%} exact conversions, max relative error={:.2e}".format(stats / N, max_err))

It is similar to the one Damien posted before, but the version tests specifically the repr function, which features additional refinements compared to a %.g format with fixed precision. This code also makes sure that no single case shows a relative error greater than the precision expected from the mantissa.

This new code brings the error rate to a really low level, which should be acceptable for MicroPython intended use.

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 3 times, most recently from aba60a3 to adcadc2 Compare June 13, 2025 14:50
@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Jun 13, 2025
@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 779f0e9 to a7c4061 Compare June 13, 2025 16:43
@yoctopuce
Copy link
Contributor Author

yoctopuce commented Jun 13, 2025

The unix / nanbox variant did originally crash at the end of the accuracy test, due to the randomly generated float number that create invalid nanbox objects.

I have therefore fixed the nanbox code in obj.h to prevent the creating bad nanobjects

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 1e547ba to 1647452 Compare June 13, 2025 21:45
py/mpprint.h Outdated
#define PF_FLAG_SEP_POS (9) // must be above all the above PF_FLAGs
#define PF_FLAG_USE_OPTIMAL_PREC (0x200)
#define PF_FLAG_ALWAYS_DECIMAL (0x400)
#define PF_FLAG_SEP_POS (16) // must be above all the above PF_FLAGs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over in the PR where the code that added PF_FLAG_SEP_POS we discussed the position of this flag. There is a port with 16-bit ints. It was possible to position PF_FLAG_SEP_POS at 9, because there were enough bits in an unsigned 16-bit value to squeeze in _ and , as shifted values without widening the type of the flags argument.

It's my mistake that I didn't comment on the requirement here, nor write a check that would trigger during CI. There is an assertion about the situation in objstr.c but because the pic16bit port is not built during CI (non-free toolchain) the assertion is only checked on platforms with 32-bit ints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goot point. I saw your (unrelated) PR on mpprint.h yesterday and was wondering about this.
I have just updated the code to move these new flags above the SEP bits.
As they are strictly related to the use of floats, which are obviously not enabled in the pic16 port, this should be safe.

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 535a56a to 153cbb5 Compare June 14, 2025 14:59
@yoctopuce
Copy link
Contributor Author

While fixing nanbox CI to avoid a crash in case of handcrafted nan floats, I run into another small nanbox bug: math.nan was incorrectly defined as a signaling nan. Using that value caused a failure of float/math_domain.py when testing copysign. So I have included that fix as well in the PR, since this was breaking CI tests.

@yoctopuce
Copy link
Contributor Author

Heads up: I found a trick to get 100% good conversions, significantly faster then CPython, and the code appears to be smaller than my previous version. Working on it...

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 314df34 to 84a001f Compare June 17, 2025 11:07
@robert-hh
Copy link
Contributor

Runtimes with the code above:

STM32 (pyboard V1.1) 0.76s
ESP32 2.62s
MIMXRT (Teensy 4.1) 0.35s
CC3200 (WiPy) 10.2s
ESP8266 7.5s
SAMD51 1.04 s
SAMD21 12.7s
Renesas-RA (EK-RA6M2) 4.04s
RP2 (RP2040) 4.39s
NRF (Arduino Nano Connect NRF82540) 1.9s

So only CC3200 and SAMD21 would be above the 10s limit. That's OK. We might not have to take care about them. All single float tests fail by exceeding the error limits at FAILED: repr rate=98.350% max_err=8.675e-08.

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 742fb2c to e4f3f8d Compare June 19, 2025 08:13
@robert-hh
Copy link
Contributor

robert-hh commented Jun 19, 2025

I will test that during the day. Just a quick compare for the two ends of the range of your recent test accuracy code with the code I used:

MIMXRT 1.4s (your code) vs 0.37s
STM32 3.06s vs 0.76s
ESP8266 39s vs 7.5 s

Note that even with your code the ESP8266 needs the changed test, that the value returned by pseudo_randouble() is a float.

max_err = 0
for _ in range(N):
f = pseudo_randouble()
while math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESP8266 needs this line to be changed to:
while type(f) is not float or math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I did not realize the problem would not be specific to randbits. I have now force-pushed a new version reusing many of yours ideas, including using randbits. I have limited the number of tests to 1500, so it should run on all platforms I guess.

Would you give it a try ?

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch 2 times, most recently from 32a6477 to 98fe95a Compare June 19, 2025 09:18
@yoctopuce
Copy link
Contributor Author

I will test that during the day. Just a quick compare for the two ends of the range of your recent test accuracy code with the code I used:

MIMXRT 1.4s (your code) vs 0.37s STM32 3.06s vs 0.76s ESP8266 39s vs 7.5 s

Note that even with your code the ESP8266 needs the changed test, that the value returned by pseudo_randouble() is a float.

randbits is definitely the best solution, agreed. I have changed the test code to use it, and added the ESP8266 check. But I will see if I can fix the array.array function (as I did for nanbox) rather than adding this extra guard, I believe it would make more sense.

@robert-hh
Copy link
Contributor

There is not much difference between rnd_buf[] being local or global. The local version is slightly faster, since it does not need a symbol lookup. So for code clarity the local version seems better. Interestingly the results for ESP8266 and SAMD21, RP2, STM32 or ESP32 at N=1200 are different. Except for SAMD21, you could go back to N=2000.

SAMD21 at N=1200:

1200 values converted
FAILED: repr rate=98.167% max_err=8.675e-08

real	0m7,237s
user	0m0,109s
sys	0m0,038s

ESP8266, N=1200:

1200 values converted
FAILED: repr rate=98.750% max_err=3.082e-07

real	0m4,653s
user	0m0,114s
sys	0m0,027s

ESP8266, N=2000:

2000 values converted
FAILED: repr rate=98.900% max_err=3.082e-07

real	0m7,240s
user	0m0,131s
sys	0m0,021s

RP2 at N=1200:

1200 values converted
FAILED: repr rate=98.167% max_err=8.675e-08

real	0m2,802s
user	0m0,106s
sys	0m0,024s

RP2 at N=2000:

2000 values converted
float format accuracy OK

real	0m4,436s
user	0m0,126s
sys	0m0,009s

@robert-hh
Copy link
Contributor

On a side note: At single precision the function pseudo_randfloat() is called 2301 times, at double precision it's 2171.

@robert-hh
Copy link
Contributor

So they all pass fine at N=2000 except for the ESP8266, where the error stays too high. I wonder what's the difference using the same float code in MP. Obviously the compiler & C-libs are different.

@yoctopuce
Copy link
Contributor Author

yoctopuce commented Jun 19, 2025

So they all pass fine at N=2000 except for the ESP8266, where the error stays too high. I wonder what's the difference using the same float code in MP. Obviously the compiler & C-libs are different.

The problem has the same root cause as the reason why you had to add type(f) is not float: REPR_C.

  • This is a nan-boxing representation, and when creating an object from float, the code does not check if the input float is a nandifferent from the nan. This is the reason why the function randfloat created non-float objects
  • Two digits are lost in the mantissa when storing objects in REPR_C...

I am looking at fixing the first issue as I did for REPR_D, but I am afraid that this would increase code size significantly given this is an inline function. So maybe we should leave your test in the test case, and I should submit this as a different PR to make the impact on code size more obvious.

Regarding the second issue, I guess I can detect REPR_C by the fact that some floats don't exist, and adjust the expectations accordingly.

@dpgeorge
Copy link
Member

I guess I can detect REPR_C by the fact that some floats don't exist, and adjust the expectations accordingly.

Yes, that's possible. tests/feature_check/float.py uses float("1.0000001") == float("1.0") to see if REPR_C is active.

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch from 98fe95a to 86f9af4 Compare June 19, 2025 12:25
@yoctopuce
Copy link
Contributor Author

yoctopuce commented Jun 19, 2025

I guess I can detect REPR_C by the fact that some floats don't exist, and adjust the expectations accordingly.

Yes, that's possible. tests/feature_check/float.py uses float("1.0000001") == float("1.0") to see if REPR_C is active.

Thanks for the hint. So I have added the exact same test in float_format_accuracy.py

Regarding the problem of hand-crafted REPR_C floats getting interpreted as objects:

  1. do you prefer that I include the fix in this PR as I did for nanbox, or that I move both to a different PR ?
  2. I am a bit concerned by the impact on code size of the extra check to add in mp_obj_new_float inline function.
    Should I rather make it non-inline for REPR_C ? I don't think the performance hit would be that big

# Deterministic pseudorandom generator. Designed to be uniform
# on mantissa values and exponents, not on the represented number
def pseudo_randfloat():
global rnd_seed, rnd_buff
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If at all, this line should be
global float_size

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides that the test passes for all boards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you VERY much for all time you have spent helping me to improve this PR, this was really helpful.

I have pushed the new version without the useless global (and moving away math.nan fix on REPR_C, as it was not strictly needed for this PR). No need to rerun the tests for now, nothing else has changed.

I will be sending a separate PR for fixing math.nanand crafted nan-floats on REPR_C and REPR_D

@dpgeorge
Copy link
Member

  1. do you prefer that I include the fix in this PR as I did for nanbox, or that I move both to a different PR ?

Please move to a different PR, this one is getting big :)

2. I am a bit concerned by the impact on code size of the extra check to add in mp_obj_new_float inline function.
Should I rather make it non-inline for REPR_C ? I don't think the performance hit would be that big

I need to think about this.

@yoctopuce
Copy link
Contributor Author

So they all pass fine at N=2000 except for the ESP8266, where the error stays too high. I wonder what's the difference using the same float code in MP. Obviously the compiler & C-libs are different.

I have pushed the code with the new REPR_C tolerance. I think this should now work on all platforms. To keep the test duration short, I have left the number of numbers to test set to 1200. As demonstrated with REPR_C, 1200 steps appear to be sufficient to detect any platform-specific discrepancy.

@yoctopuce
Copy link
Contributor Author

  1. do you prefer that I include the fix in this PR as I did for nanbox, or that I move both to a different PR ?

Please move to a different PR, this one is getting big :)

@dpgeorge I have submitted a PR with the trivial fix for math.nan and hand-crafted floats in nanbox. Once it will be integrated into master, I can remove that part of the code from this PR and rebase to the new master.

I have intentionally kept the REPR_C crafted-floats issue separate, as more time will be needed to find the most efficient implementation. The test code is already available here: edf5cab

@yoctopuce yoctopuce force-pushed the improve_formatfloat branch from 11a8e87 to 86fbed2 Compare June 23, 2025 15:05
@yoctopuce
Copy link
Contributor Author

I have rebased the PR to master and removed nanbox-related changes, now that they have been integrated in master.

py/mpprint.c Outdated
@@ -353,6 +353,14 @@ int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, unsigned int

char *s = buf;

if ((flags & PF_FLAG_ALWAYS_DECIMAL) && strchr(buf, '.') == NULL && strchr(buf, 'e') == NULL && strchr(buf, 'n') == NULL) {
if ((size_t)(len + 2) < sizeof(buf)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to remove this buffer size check by allocating 2 more bytes to buf, and then passing sizeof(buf) - 2 to mp_format_float() above, thereby guaranteeing there will always be enough space to add these characters?

Same comment for the PF_FLAG_ADD_PERCENT check below, so add 3 bytes in total to buf and actually pass sizeof(buf) - 3 to mp_format_float().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent idea, I have changed that.

py/mpconfig.h Outdated
// Float conversion to/from string implementations
#define MICROPY_FLTCONV_IMPL_BASIC (0) // smallest code, but inexact
#define MICROPY_FLTCONV_IMPL_APPROX (1) // slightly bigger, almost perfect
#define MICROPY_FLTCONV_IMPL_EXACT (2) // bigger code, but 100% exact repr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use "and 100% exact repr" (instead of "but") because it's a positive thing that it's an exact representation.

py/mpconfig.h Outdated
#endif

#ifndef MICROPY_FLTCONV_IMPL
#define MICROPY_FLTCONV_IMPL (MICROPY_FLOAT_DEFAULT_FLTCONV_IMPL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this MICROPY_FLOAT_DEFAULT_FLTCONV_IMPL needed? Why not just wrap the above bit of code in #ifndef MICROPY_FLTCONV_IMPL and then define MICROPY_FLTCONV_IMPL directly?

Also, maybe call it MICROPY_FLOAT_FORMAT_IMPL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, done

@dpgeorge
Copy link
Member

dpgeorge commented Jul 1, 2025

This is looking very good now! My testing on PYBD-SF2 (single prec) and PYBD-SF6 (double prec) shows that everything is good, all tests pass.

And around +250 bytes code size is pretty decent for the benefit provided by the approx algorithm.

Definitely mpy-cross should use the exact algorithm, because that benefits all targets and the cost (code size and speed) is host side. Could you enable that?

Following discussions in PR micropython#16666, this commit updates the
float formatting code to improve the `repr` reversibility,
i.e. the percentage of valid floating point numbers that
do parse back to the same number when formatted by `repr`.

This new code offers a choice of 3 float conversion methods,
depending on the desired tradeoff between code size and
conversion precision:
- BASIC method is the smallest code footprint
- APPROX method uses an iterative method to approximate
  the exact representation, which is a bit slower but
  but does not have a big impact on code size.
  It provides `repr` reversibility on >99.8% of the cases
  in double precision, and on >98.5% in single precision.
- EXACT method uses higher-precision floats during conversion,
  which provides best results but, has a higher impact on code
  size. It is faster than APPROX method, and faster than
  CPython equivalent implementation. It is however not available
  on all compilers when using FLOAT_IMPL_DOUBLE.

Here is the table comparing the impact of the three conversion
methods on code footprint on PYBV10 (using single-precision
floats) and reversibility rate for both single-precision and
double-precision floats. The table includes current situation
as a baseline for the comparison:

          PYBV10    FLOAT   DOUBLE
current = 364136   27.57%   37.90%
basic   = 364188   91.01%   62.18%
approx  = 364396   98.50%   99.84%
exact   = 365608  100.00%  100.00%

Signed-off-by: Yoctopuce dev <[email protected]>
@yoctopuce yoctopuce force-pushed the improve_formatfloat branch from 86fbed2 to 7034fad Compare July 3, 2025 06:32
@yoctopuce
Copy link
Contributor Author

Definitely mpy-cross should use the exact algorithm, because that benefits all targets and the cost (code size and speed) is host side. Could you enable that?

The default settings in mpconfig.h should enable EXACT mode automatically by default for mpy-cross when using gcc, because mpy-cross is using IMPL_DOUBLE and long double is available. On Windows, there is no true long double, so APPROX is the best we can do anyway...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
py-core Relates to py/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants