Picolibc Without Double
Smaller embedded processors may have no FPU, or may have an FPU that only supports single-precision mode. In either case, applications may well want to be able to avoid any double precision arithmetic as that will drag in a pile of software support code. Getting picolibc to cooperate so that it doesn't bring in double-precision code was today's exercise.
Take a look at the changes in git
__OBSOLETE_MATH is your friend
The newlib math library, which is where picolibc gets its math code, has two different versions of some functions:
- single precision sin, cos and sincos
- single and double precision exp, exp2 and log, log2 and pow
The older code, which was originally written at Sun Microsystems (most of this code carries a 1993 copyright), is quite careful to perform single precision functions using only single precision intermediate values.
The newer code, which carries a 2018 copyright from Arm Ltd, uses double precision intermediate values for single precision functions.
I haven't evaluated the accuracy of either algorithm, but the newer code claims to be faster on machines which support double in hardware.
However, for machines with no hardware double support, especially for machines with hardware single precision support, I'm betting the code which avoids double will be faster. Not to mention all of the extra space in ROM that would be used by a soft double implementation.
I had switched the library to always use the newer code while messing about with some really stale math code last month, not realizing exactly what this flag was doing. I got a comment on that patch from github user 'innerand' which made me realize my mistake.
I've switched the default back to using the old math code on platforms
that don't have hardware double support, and using the new math code
on platforms that do. I also added a new build option,
-Dnewlib-obsolete-math
, which can be set to auto
, true
, or
false
. auto
mode is the default, which selects as above.
Float vs Double error handling
Part of the integration of the Arm math code changed how
newlib/picolibc handles math errors. The new method calls functions to
set errno and return a specific value back to the application, like
__math_uflow
, which calls __math_xflow
which calls
__math_with_errno
. All of these versions take double parameters and
return double results. Some of them do minor arithmetic on these
parameters. There are also float versions of these handlers, which
are for use in float operations.
One float function, the __OBSOLETE_MATH version of log1pf
, was mistakenly using the double
error handlers, __math_divzero
and __math_invalid
. Just that one
bug pulled in most of the soft double precision implementation. I
fixed that in picolibc and sent a patch upstream to newlib.
Float printf vs C ABI
The C ABI specifies that float parameters to varargs functions are always promoted to doubles. That means that printf never gets floats, only doubles. Program using printf will end up using doubles, even if there are no double values anywhere in the code.
There's no easy way around this issue — it's hard-wired in the C
ABI. Smaller processors, like the 8-bit AVR, “solve” this by simply
using the same 32-bit representation for both double
and float
.
On RISC-V and ARM processors, that's not a viable solution as they
have a well defined 64-bit double type, and both GCC and picolibc need
to support that for applications requiring the wider precision.
I came up with a kludge which seems to work. Instead of passing a float parameter to printf, you can pass a uint32_t containing the same bits, which printf can unpack back into a float. Of course, both the caller and callee will need to agree on this convention.
Using the same mechanism as was used to offer printf/scanf functions
without floating point support, when the #define
value,
PICOLIBC_FLOAT_PRINTF_SCANF
is set before including stdio.h, the
printf functions are all redefined to reference versions with this
magic kludge enabled, and the scanf functions redefined to refer to
ones with the 'double' code disabled.
A new macro, printf_float(x)
can be used to pass floats to any of
the printf functions. This also works in the normal version of the
code, so you can use it even if you might be calling one of the
regular printf functions.
Here's an example:
#define PICOLIBC_FLOAT_PRINTF_SCANF
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
printf("pi is %g\n", printf_float(3.141592f));
}
Results
Just switching to float-only printf removes the following soft double routines:
- __adddf3
- __aeabi_cdcmpeq
- __aeabi_cdcmple
- __aeabi_cdrcmple
- __aeabi_d2uiz
- __aeabi_d2ulz
- __aeabi_dadd
- __aeabi_dcmpeq
- __aeabi_dcmpge
- __aeabi_dcmpgt
- __aeabi_dcmple
- __aeabi_dcmplt
- __aeabi_dcmpun
- __aeabi_ddiv
- __aeabi_dmul
- __aeabi_drsub
- __aeabi_dsub
- __aeabi_f2d
- __aeabi_i2d
- __aeabi_l2d
- __aeabi_ui2d
- __aeabi_ul2d
- __cmpdf2
- __divdf3
- __eqdf2
- __extendsfdf2
- __fixunsdfdi
- __fixunsdfsi
- __floatdidf
- __floatsidf
- __floatundidf
- __floatunsidf
- __gedf2
- __gtdf2
- __ledf2
- __ltdf2
- __muldf3
- __nedf2
- __subdf3
- __unorddf2
The program shrank by 2672 bytes:
$ size double.elf float.elf
text data bss dec hex filename
48568 116 37952 86636 1526c double.elf
45896 116 37952 83964 147fc float.elf