The common programmer has no clue about the numerical instabilities that floating point numbers are causing.
Your examples are exactly what I would expect from a capable engineer that is pondering the different pros and cons when you have to optimize your code.
Calling BigDecimals entirely unneeded tells me you are in a luxury position where you can draw from a pool of the best programmers that are out there.
For lesser programmers and for the areas where speed is not as important, BigDecimal solves the issues completely and without any clever thinking and most importantly, consistently correct without having to rely on the skills of the programmer.
I will agree BigDecimal is fine to use if speed really doesn't matter, it's at least correct even if it's a nuclear option of sorts. What I really want to discourage is using fixed integer to represent some smallest unit of money, like using an int where 100 = 1 dollar, 1010 = 10 dollars and 10 cents. That is fundamentally problematic and objectively worse than using a floating point value.
But sure, if you genuinely don't want to think at all about the issue, use your language's BigDecimal. If you decide you want the much improved performance, then use a floating point value, either the IEEE decimal floating point standard or use binary floating point to represent some reasonably small unit fraction of a dollar, like 1.0 = 10^-6 dollars.
Ints wont cut it there.
Even if you are dealing in units of hundreds of billions of dollars tracked to mils, this gets you up to, I think, a hundred-thousandth of a percent with int64.
If so: totally agreed. Inflexible single-scaled-ints have been a plague in nearly every piece of production code I've seen them in. I'll take floats (well, doubles) any day over this.
Flexible scale tho (arbitrary precision / bigdecimal equivalents (I'd prefer bigdecimal over home-grown two-int variants, obv)) usually works out. And tends to avoid semi-frequent-though-usually-unimportant/unnoticed[1] issues with imprecise floats (~16 million for exact int representation[2] is awfully small, though doubles are generally Good Enough™).
[1]: usually. adding doubles/fractions often produces garbage-looking output unless you remember to round. but in my lines of work, nobody cares[1.1] if it's a couple cents off in analytics.
[2]: https://stackoverflow.com/questions/3793838/which-is-the-fir...
[1.1]: yet
Generally though, I think people don't quite do due-diligence here. And/or the domain changes enough that numbers get large enough to invalidate the earlier analysis. There are obviously exceptions to this though, and for them, they know what they're doing and why they're doing it and it's all good.
Kranar: > working in the financial sector (high frequency trading)
Okay, you got me there - you have pointed me to that single application from the financial sector where it is not totally acceptable to "waste" a few thousands of processor cycles to compute some money-related stuff. Granted, in HFT applications, these cycles may allow you to get your trades in front of those of the other HFT companies. You are forced to use CPU-accelerated (or maybe even GPU?) computations in this case, which automatically means "floating point".
But there's another difference that allows you do do this: you mostly don't have to care about exact, accurate-up-to-the-penny results. I assume most of your calculations are done to eventually arrive at a conclusion of whether to buy or sell some stock or not, and at which price. You have to take care of not accumulating too much rounding errors in the process, of course, but the threshold for these errors is set by yourself, and you can give yourself a bit of leeway on this, because it's all internal stuff, mostly probabilistic and statistics-based - the only stuff that may be audited by someone else and thus has to match the real numbers up to the last penny are the trades you do and the money you move around, and I bet all of this accounting-style stuff is recorded using...decimal numbers :D
I work in the retail industry (think cash registers, retail sale accounting, that kind of stuff) and pretty much any legislation on this planet would obliterate us if we'd tell them that the result of the computations of our systems may be some cents up or down from the real result - the one someone would get who just scribbled the numbers on a sheet of paper and added them up manually. Our customers have to pay taxes based on the sales that they account using our systems, and the tax calculations as well as the entire chain of processes they use to arrive at the final numbers are regularly audited by the respective governments. There are specific rules (of course differing by country) as to how many decimal places have to be used and how stuff has to be rounded in which particular cases in calculations that would require more decimals. We waste an enormous amount of CPU cycles just to strictly adhere to these rules - and that is not only absolutely necessary, but also totally okay, modern CPUs can easily accomodate this in our scenario.
It is especially in cases like this that you absolutely should not use fixed decimal point systems to represent money. It is exactly in these circumstances that your fixed decimal point system will eventually encounter a situation where it fails catastrophically.
Intel provides an IEEE decimal floating point system specifically for the purpose of adhering to legal requirements, they say so right at the top of their website:
https://software.intel.com/en-us/articles/intel-decimal-floa...
If fixed point math would have solved this issue, they'd have provided a solution involving that, but fixed point math is simply the absolute worst solution to this problem, even worse than naively using a 64-bit binary floating point number.
BigDecimal is also a viable solution, but it's entirely unnecessary and the cost really is enormous especially if you need to handle a large number of transactions. But sure, if you say performance genuinely isn't an issue go with BigDecimal.
That is exactly what we currently do, and speed of computations is definitely not an issue at all. If we have performance related problems (and we do sometimes), they have never in 10 years originated from numerical computation just taking too long, but always from other inefficiencies, quite often of architectural nature that uselessly burn a thousand times more cycles than the numeric part of the job.
BigDecimal provides exactly what we need: a no-brainer, easy-to-use, easy-to-understand and always correct vehicle for the calculation of monetary sums. It lets our developers focus on implementing the business logic, and also on making less of the described high-level failures that get us into actual performance troubles, instead of consuming a lot of mental share just to “get the computation right“ in all circumstances.
Hell, floating point isn't even a good way of doing imprecise arithmetic on larger numbers. What you would actually want is log arithmetic, but that's not what the hardware implements (even though it's simpler). It gives you roughly the same overall precision over each range but stops it from suddenly being cut in half as the exponent changes.
But you are not some cents up or down, even if you are calculating with billions, you still have 7 significant digits, that's a lot of head room for calculations.
Are there any financially sensible calculations where large numbers (order of magnitude billions) are multiplied with each other?
Would you care to elaborate on some of these issues? I'm genuinely curious.
When I started working in finance straight out of school (not in HFT at the time), I naively accepted the dogma that money should never be represented using floating points. I mean everywhere I went I would read in bold letters don't use floating point! Don't use floating point! So I just accepted it to be true and didn't question it. When I wrote my first financial application I used an int64 to represent currencies with a resolution of up to 10^-6 because that was what everyone said to do.
And well... all was good in life. Then one day I extended my system to work with currencies other than U.S. dollars, like Mexican Peso's, Japanese Yen, and currencies X where 1 X is either much less than 1 USD or much greater than 1 USD.
Then things started to fail, rounding errors became noticeable, especially when doing currency conversions, things started to fall apart real bad.
So I took the next logical step and used a BigDecimal. Now my rounding issues were solved but the performance of my applications suffered immensely across the board. Instead of storing a 64 bit int in a database I'm now storing a BigDecimal in Postgresql, and that slowed my queries immensely. Instead of just serializing raw 64-bits of data across a network in network byte order, I now have to convert my BigDecimal to a string, send it across the wire, and then parse back the string. Every operation I perform now requires potentially allocating memory on the heap whereas before everything was minimal and blazingly fast. I feel like there is a general attitude that performance doesn't matter, premature optimization is evil, programmer time is more expensive than hardware, so on so forth... but honestly nothing feels more demoralizing to me as a programmer then having an application run really really fast one day, and then the next day it's really really slow. Performance is one of those things that when you have it and know what it feels like, you don't want to give it up.
So not knowing any better I decided to come up with a scheme to regain the lost performance... I realized that for U.S. dollars, 10^-6 was perfectly fine. For currencies that are small compared to the U.S. dollar, I needed fewer decimal places, so for Yen, 1 unit would represent 10^-4 Yen. For currencies bigger than U.S. dollar, 1 unit would represent 10^-8...
So my "genius" younger self decided that my Money class would store both an integer value and a 'scale' factor. When doing operations, if the scale factor was the same, then I could perform operations as is. When the scale factor was different, I would have to rescale the value with lower precision to the value with higher precision and then perform the operation.
This actually worked to a degree, I regained a lot of my speed and didn't have any precision issues, but all I did was reinvent a crappy floating point system in software without knowing it.
Eventually I ended up reading about actual floating points and I could see clearly the relationship between what I was doing and what actual experts had realized was the proper way to handle working with values whose scales could vary wildly.
And once I realized that I could then sympathize with why people were against using binary floating point values for money, but the solution wasn't to abandon them, it was to actually take the time to understand how floating point works and then use floating points properly for my domain.
So my Money class does use a 64-bit floating point value, but instead of (double)(1.0) representing 1 unit, it represents 10^-6 units. And it doesn't matter what currency I need to represent, I can represent currencies as small as Russian rubles to as large as Bitcoin, and it all just works and works very fast.
64-bit doubles give me 15 digits of guaranteed precision, so as long as my values are within the range 0.000001 up to 999999999.999999 I am guaranteed to get exact results.
For values outside of that range, I still get my 15 digits of precision but I will have a very small margin of error. But here's the thing... that margin of error would have been unavoidable if I used fixed decimal arithmetic.
Now I say this as a personal anecdote but I know for a fact I'm not the only one who has done this. I just did a Google search that led to this:
https://stackoverflow.com/questions/224462/storing-money-in-...
The second top answer with 77 points yells in bold letters about not using floating points as a currency and suggests using a scheme almost identical to the one I described, where you store a raw number and a scaling factor, basically implementing a poor-man's version of floating point numbers.
I guess you fall into the few percent of developers who are NOT meant to be addressed by the general rule of "don't use float for money". Like with all "general rules" in software development, it is intended to guide the >90% of devs who don't want and sometimes also aren't capable of fully grasping the domain they're working in and the inner workings of the technology they use. The majority of devs need simple, clear guidelines that prevent them from making expensive mistakes while wielding technology that's made up of layers and layers of abstractions, of which some (or even most) are entirely black boxes to them. "Playing it safe" often comes with other shortcomings like worse performance, and if those are not acceptable in a specific scenario, you need a developer who fully grasps the problem domain and technology stack and who is thus capable of ignoring the "general rules" because he knows exactly why they exist and why he won't get into the troubles they are intended to protect you from.
I have long thought that every developer under the sun should strive to get up to this point, and I still think that it is an admirable goal, but I came to understand that not all developers share this goal, and that even if someone tries to learn as much as possible about every technology that he comes in contact with, he will never be able to reach this state of deep understanding in every technical domain imaginable, as there are just too many of them nowadays. We all, no matter how smart we are, sometimes need to rely on "general rules" in order to not make stupid mistakes.
https://www.postgresql.org/docs/current/static/datatype-nume...
If you specify a precision and scale, performance of NUMERIC improves quite a bit, but now you can’t store values with vastly different magnitudes (USD and JPY) in the same column without wasting tons of storage on each row. You’re back to square one.
That being said, I agree with your sentiment that there are faster/more efficient ways to accomplish this. For something like smart contracts, I'd think they should be able to abstract this line of reasoning into an API that's consumable for those of us that aren't as familiar. Or is at least worth having the option of.
I feel like this is constantly repeated but is simply untrue and working in the financial sector (high frequency trading) I don't know anyone who actually uses fixed digit number systems to represent money. Normally what I do see are people outside of finance who need to represent money who read the common mantra about using a fixed digit representation only to eventually encounter all the issues that floating point was invented to solve. When they encounter those issues they then end up having to adapt their code only to basically re-invent a broken, unstable quasi-floating point system when they would have been much better off using the IEEE floating point system and actually taking the time to understanding how it works.
BigDecimal will solve the issue but it's absolute overkill both in terms of space and time, we're talking many many orders of magnitude in terms of performance degradation and it's entirely unneeded.
One simple solution that works very well for up to 6 decimal places is to take your idea of having an implied number of digits, but instead of using an integer data type, you use a floating point data type. So 1 dollar and 5 cents becomes (double)(1050000.0). This lets you represent a range from 0.000001 up to 9999999999.999999 exactly, without any loss of precision. Values outside of that range can still be represented as well but you may have some precision issues.
Another solution which is less efficient but more flexible is to use decimal floating point numbers instead of binary floating point numbers. There is an IEEE decimal floating point standard that can represent an enormous range of decimal values exactly. Intel even provides a C/C++ implementation that meets formal accounting standards.
Both of the solutions I list above are orders of magnitude more efficient than using fixed-point numeric types and are by no means difficult to use.