6

I was trying a very simple printf test:

printf "%.16f\n" 5.10

On linux I got this output: 5.100000000000000000 which is expected.

But the very same test on OS/X produces this: 5.0999999999999996

Why does printf produce different output?

EDIT: This is not C code, printf is also a command-line utility, handy for scripts and tests.

The equivalent C program below produces 5.100000000000000000:

#include <stdio.h>

int main() {
    printf("%.16f\n", 5.10);
    return 0;
}

EDIT 2: The plot thickens... to make this more interesting for linux users, if I run this command as nobody, I get the same behavior as on OS/X:

chqrlie$ printf "%.18f\n" 5.10
5.100000000000000000
chqrlie$ su nobody -c 'printf "%.18f\n" 5.10'
5.099999999999999645
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • Does the C program you show produce the same result on both Linux and macOS? If it does then the problem is either in the implementation of the `printf` commands, or in the shells. Which shells are you using on each system? – Some programmer dude Apr 12 '19 at 08:06
  • 2
    Yes, I get different output on different OS, but the C program produces the same output on both OSes :) – chqrlie Apr 12 '19 at 08:07
  • the simple answer would be that the macos chain of shell utilities is from the 80s (before GPL) and broken. Use GNU/Linux. Would you accept that? – hek2mgl Apr 12 '19 at 08:09
  • @hek2mgl: the utility chain does behave differently, but is it a bug or a feature? – chqrlie Apr 12 '19 at 08:10
  • @chqrlie this is not a bug nor a feature, it is inherent to floats! 5.1 can't be stored exactly, then any printing not so far from 5.1 is acceptable. Don't be fooled by the fact that some printing function gives you 5.1 in return... – Jean-Baptiste Yunès Apr 12 '19 at 08:17
  • @chqrlie It has everything to do with [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) You understand that there is no canonical 16-digit stringification of the floating point number produced by parsing the string "5.10", right? That the details of that are entirely implementation-dependent? – Sneftel Apr 12 '19 at 08:23
  • 1
    With `printf(1)`, I get 5.099999999999999645 on OS X and Linux, without having to do anything weird with sudo. EDIT: printf is a shell builtin (zsh on linux, bash on OS X). Using /usr/bin/printf gives 5.10... on linux, and the long number on OS X) – Shawn Apr 12 '19 at 08:25
  • Looks like some implementations use `long double` instead of `double`. – chtz Apr 12 '19 at 08:26
  • 2
    When 5.10 is stored as a 64-bit IEEE-754, the exact value is 5.0999999999999996447286321199499070644378662109375. If you print that with 16 digits after the decimal, the correctly rounded value is 5.0999999999999996. So the only question is why does the linux command print something other than that. – user3386109 Apr 12 '19 at 08:29
  • 1
    I reopened this question. The difference between printf on different OS and between the host's C library itself(!) is indeed interesting and if I understood correctly the main point the OP is asking about – hek2mgl Apr 12 '19 at 08:30
  • 3
    Are you using the same shells (and versions) on both systems? – andreee Apr 12 '19 at 08:42
  • 2
    `printf` is a built-in command in some shells (e.g. Bash). The users "chqrlie" and "nobody" may be using different shells, one with a built-in `printf` command and one without. – Ian Abbott Apr 12 '19 at 10:32
  • [Floating point rounding in shell](https://stackoverflow.com/q/12113612/608639) – jww Apr 12 '19 at 12:26
  • I'm on an iMac running High Sierra and when I run printf "%.16f\n" 5.10 I get 5.1000000000000000 – Natsfan Apr 12 '19 at 15:56
  • 1
    I agree with above that you're running different versions. for each user try `type printf`. You may get `printf is a shell builtin` OR `printf is /usr/bin/printf`. Then you can research from there. Good luck. – shellter Apr 12 '19 at 17:03

2 Answers2

8

Both the GNU implementation and the MacOS (FreeBSD) implementation of printf are different programs. Both aim to be compatible with the POSIX standard.

POSIX leaves the representation of floating point numbers open to the implementation of printf. Their argumentation is that all calculation in shell is integer anyway.

The floating-point formatting conversion specifications of printf() are not required because all arithmetic in the shell is integer arithmetic. The awk utility performs floating-point calculations and provides its own printf function. The bc utility can perform arbitrary-precision floating-point arithmetic, but does not provide extensive formatting capabilities. (This printf utility cannot really be used to format bc output; it does not support arbitrary precision.) Implementations are encouraged to support the floating-point conversions as an extension.

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html


PS:

5.1

is not a floating point number in bash. bash does not support floating numbers.

5.1 is a string, interpreted by printf depending on the locale(!)

theymann@theymann-laptop:~/src/sre/inventory-schema$ LANG=en_US.UTF8 printf "%.16f\n" 5.10
5.1000000000000000
theymann@theymann-laptop:~/src/sre/inventory-schema$ LANG=de_DE.UTF8 printf "%.16f\n" 5.10
bash: printf: 5.10: Ungültige Zahl. # << German: Bad Number
0,0000000000000000

Note: In Germany we use , as the decimal separator.


The difference in the output between a normal user and nobody must the shell which is used. some shells, like busybox come with their own implementation of printf. Btw, I'm extremely surprised that nobody is allowed to execute commands on your system!

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • You seem to have a good track, but how do you explain the difference observed in my latest EDIT – chqrlie Apr 12 '19 at 08:23
  • @chqrlie may be one implementation is not calling C with the same format string... – Jean-Baptiste Yunès Apr 12 '19 at 08:26
  • 1
    Looks like your shell and the system shell are different. – hek2mgl Apr 12 '19 at 08:42
  • @chqrlie is the same _printf_ command called or not when it is you / nobody ? If yes what difference do you see when you execute _printf_ under _strace_ in the two cases (to explain a diff in the configurations) – bruno Apr 27 '19 at 16:54
  • 1
    @bruno: the difference is intriguing indeed... The reason is `nobody`'s default shell is `/bin/sh` and mine is `/bin/bash`. `/bin/sh` uses an internal implementation of the `printf` command which uses `float` arithmetic whereas `/usr/bin/printf` uses `double` arithmetics on GNU/linux and `float` arithmetics on BSD based OS/X – chqrlie Apr 27 '19 at 21:57
3

This is because floating point types mostly can't represent exact values. For example using an online IEE754 tool you got:

enter image description here

So 5.1 is not exactly representable using this format.

Then printf (or whatever) is free to format/print any value that it thinks suitable for the user.

Jean-Baptiste Yunès
  • 34,548
  • 4
  • 48
  • 69
  • While your answer has valid points, it does not explain the observed behavior. I am requesting 16 decimals, is the `printf` utility bogus on OS/X? – chqrlie Apr 12 '19 at 08:09
  • That does not explain why the `printf` command gives 5.0999999999999996 while the printf/() C call gives 5.100000000000000000 on osx. Nor does it explain why the `printf` command gives different output on osx vs linux. – nos Apr 12 '19 at 08:11
  • No, OS/X printf is just fair enough and gives you the representation of the stored value. I personally dislike very much this kind of printf cheating... – Jean-Baptiste Yunès Apr 12 '19 at 08:11
  • @nos every implementation is free to print the value as it wants, floatings are not exact... You may think "rounding" to 5.1 on print is a good thing but it is not at this cheats on you what is stored. – Jean-Baptiste Yunès Apr 12 '19 at 08:13
  • 1
    @Jean-BaptisteYunès: come on, the behavior of floating point math is much more precisely defined than you infer. – chqrlie Apr 12 '19 at 08:22
  • @chqrlie What are you talking about? If the value stored is 5.0999999904... what was the initial value? You can't tell, so printing 5.1 is just cheating. – Jean-Baptiste Yunès Apr 12 '19 at 08:24
  • 1
    @chqrlie the printing of floats is not so well defined. Math is well defined but printing is not math. – Jean-Baptiste Yunès Apr 12 '19 at 08:27
  • 2
    @chqrlie That's incorrect. Proper conversion from a string to a floating-point number is well-defined. In the other direction, it is absolutely not. As an example, Python 2.6 and Python 2.7 differ in conversion behavior. – Sneftel Apr 12 '19 at 08:59
  • @Jean-BaptisteYunès: “Printing” is conversion. It is a conversion from one representation to another, whether it is integer to hexadecimal or floating-point to decimal. As with all conversions, the result should be the representable value closest to the mathematical result (e.g., when 16 digits are request, the number with 16 digits closest to the source operand). Conversion operations are defined in IEEE 754, and good algorithms for implementing them are known, so programming languages and other software should bind to IEEE 754. – Eric Postpischil Apr 12 '19 at 11:30
  • The operation of `printf` (C or shell) when formatting floating-point data as decimal is the operation contemplated by IEEE 754 5.12, “Details of conversion between floating-point data and external character sequences.” – Eric Postpischil Apr 12 '19 at 12:11
  • @EricPostpischil Maybe you know that there is no canonical decimal writing of all real numbers... Even "simplest" ones like 1. – Jean-Baptiste Yunès Apr 12 '19 at 18:54
  • @Jean-BaptisteYunès: That has no relevance. The question here is what the digits are, not whether scientific notation or fixed-point is used or whether an unnecessary “+”, leading “0”, or trailing “.” are used. – Eric Postpischil Apr 13 '19 at 11:07
  • @EricPostpischil No, everybody knowns that (for example) 1 and 0.999999.... are different writings of the same number, so which one to write? You have the choice. In the case of "approximations" the problem is worst... How one can decide that 5.0999999906... is 5.1 ? May be it is not! – Jean-Baptiste Yunès Apr 14 '19 at 10:18
  • Between “1” and ”0.999…”, “1” is the canonical form. Furthermore, if one were considering producing the “0.999…” form, the rounding rules would result in “1”. The way one can decide that 5.0999999906... is not 5.1 is to observe there is a non-zero difference between them; they differ by more than .0000000002. Please stop this nonsense. Good behavior is well known to floating-point practitioners; it is documented, specified, presented in textbooks, and well established. – Eric Postpischil Apr 14 '19 at 10:30