7 ms·
Seems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying <1. Not sure in what case it makes s
by zelphirkalt 4y ago
Seems all that could have been avoided by using rationals in general and the condition for living being >0 instead of dying <1. Not sure in what case it makes sense to have dying at smaller than 1. To the user all the numbers are integers, so why not use rationals instead of floats?
The more I write code, the more I notice how rarely one truly should want floats in most kinds of programs and how they almost always carry a whole bag of problems with them.
- DaiPlusPlus 4y ago> Seems all that could have been avoided by using rationals in general It's 2023: I'm still surprised that so many new languages/platforms still only provide only basic integer and IEEE-754 types for numerics: Rust, C#, Java, Swift and others still lack built-in, runtime-provided, nor even standard-library-provided rational types, which I assume really should be the preferred type for most business/domain/application values, exactly for things like RTS game unit health, for example. (and Java's lack of operator overloading makes this even more painful). ---- On a related note, can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types, and IEEE-754 types should be smarter about when NaN can happen - and it should support interval types (and evaluating interval-type-based contract invariants): this would eliminate whole classes of bugs in the first place (too many programmers think single/double is appropriate for storing currency values, ugh).
- PhilipRoman 4y agoAren't rationals subject to exponential blowup in storage/precision? IMO the best approach for business logic values is picking the smallest meaningful unit and representing it with integers. Precision and storage requirements become very easy to reason about.
- DaiPlusPlus 4y ago> IMO the best approach for business logic values is picking the smallest meaningful unit and representing it with integers. How would you design an RTS unit stats system to avoid this AoE Monk HP bug using only integer types?
- bruce343434 4y agoHow much health precision do you need? Make hp an int. Or like a /64 fixed point.
- account42 4y agoCongratulations, you just introduced a new damage threshold / resistance threshold / etc where things effectively do no damage. Fixed point would not have helped here. Integer would only have helped because dividing first would break immediately.
- falcor84 4y agoWell, in this case, if a unit is converted to a different type and back, it would just keep its (absolute) value of HP (in this case 1), no need to scale anything.
- db48x 4y agoYou haven’t thought it through yet. In this case, HP _is_ an integer, and so is the MaxHP for the unit type. And yet the bug still occurs! Storing the HP as an integer does not prevent the problem from ever occurring, because fundamentally the user still expects HP to be proportional. That is, suppose a unit is sitting there at full health, and you research a tech that increases that unit’s max hp? Should the unit now be at less than full health, as if it had been in combat? Users probably don’t like that. Suppose it is at full health, and then it gets downgraded to a type with less maximum health. Does it stay at it’s current HP? Users probably won’t like being attacked with supercharged units that have extra HP; they’ll think that the other player was cheating somehow. What if it is damaged and at half health, then gets upgraded. Should it be fully healed? Gain HP equal to the difference between the new and old maximum HP? Or should it gain half of that, so that it stays at half health? Or perhaps HP is too limiting, and the game should do what Dwarf Fortress does. DF knows the approximate surface area of every body part (based on each creature’s body plan, plus individual stats such as strength, fatness, size, etc), and the size of every weapon. Every attack therefore deals damage to a certain area, measured in square inches, and individual body parts will be destroyed once a sufficient percentage of that surface area is damaged by wounds. Or maybe you are designing this game in the 90’s, and you have to worry about squeezing unit updates for 200 units per player into the bandwidth provided by the average modem of the day (probably 28.8kbaud), so you stick to one integer because it’s the simplest think that can possibly work, and the number of bytes per update can be calculated in advance. And then some other schmuck gets stuck with the job of handling unit type changes (but be warned that the schmuck might be yourself in six months).
- rini17 4y agoDid this exponential blowup ever prove troublesome in practice? I think it can be easily solved by strategically placed rounding steps. But explicit rounding is advisable in many cases anyway.
- jrochkind1 4y agoBut strategically placed rounding was in fact the solution to the original problem that in this thread Rationals were proposed as solving... Rational types are not super popular and don't get used that much, most (not all) people that end up using them do it very intentionally and consciously and know what they're dealing with -- if they were more ubiquitious, I suspect more trouble would be seen "in practice". While it's probably true that any digital number format will have edge cases that in fact come up in practice, my personal choice of "Why the heck isn't this more popular, why doesn't every language support it, why isn't it in fact the default representation of a numeric literal" -- is floating point "decimal" types, like ruby (or I think Java?) BigDecimal, rather than rational. I think they mostly work matching programmer's mental models of numbers, and for many/most common uses on 2023 platforms the performance is just fine. (this would not have been true 30 years ago).
- rini17 4y agoOr it's vendors that don't care about correctness, like providing decimals by default and high performance floating point as an "optimized" option. Python did something like that by using bigints as a default for integers. What would not have been true 30 years ago? I remember "real" fixed-point type built-in in Turbo Pascal and explicitly documented as suitable for money. Common Lisp has had first class fractions support since the start.
- jrochkind1 4y agoI meant to suggest that 30 years ago the performance difference between "floats" (floating-point binary) and "BigDecimal"-style arbitrary-precision floating-point decimal would have been much more significant to many more real-world use cases, compared to now. So that may have been a reason not to make them the default when you simply write a literal `5.4` in code, but that argument is less now.
- Kiro 4y ago> and Java's lack of operator overloading makes this even more painful Why?
- bombolo 4y agoBecause you can't make your own types that work better.
- hgsgm 4y agoOf course you can. You just can't use operators for them. Unless you go Haskell where anything can be an operator name, operators are just a minor convenience hack .
- bombolo 4y agoSo you can but you need to rewrite everything instead of just changing a type, and the code will look completely impossible to read.
- DaiPlusPlus 4y agoAnd now all your simple scalar values need to exist on the heap as class object instances - and will incur plenty of runtime allocation costs for every arithmetic operation - or try to risk it with non-immutable heap-allocated objects, but that’s a recipe for disaster: no-one should ever think mutable shared state is a good first-design. A lot of Java/JVM’s design decisions made sense in the mid-1990s, such as not supporting user-defined stack-allocated value-types and only supporting GC-managed objects - as main-memory was fast-enough compared to CPU registers - and many/most Java targets didn’t have any kind of CPU data cache - the idea of using the heap for basically everything wasn’t a bad design… back then. But now it is: high-perf code for the CLR means eliminating unnecessary GC allocation wherever possible - but in the JVM that isn’t possible.
- cwillu 4y agoSome aspects of the problem, exagerated for effect: boolean isDead = (new I("0.9")).divide(new I("55")).multiply(new I("0.1")).lessThan(new I("1"))
- jacquesm 4y ago> can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types You mean like COBOL?
- marcosdumay 4y agoYep, like COBOL, or SQL. But, in fact, I don't know of any language that doesn't. They are just not basic types.
- DaiPlusPlus 4y agoVery few SQL implementations have unsigned integers (I think only MySQL+MariaDB has it out-of-the-box?). From my conversations with the MS SQL Server dev team, other than the overall lack-of-demand, it’s because ISO SQL leans very heavily on implicit conversions, and implicit conversions between signed and unsigned types are a massive pain to implement and gracefully handle all edge-cases and in a way that doesn’t behave too unexpectedly (and remember that most SQL language users are likely unfamiliar with all the gotchas of C-style unsigned integers: most SQL users are business analysts, not low-level systems programmers). Besides, if you just want to disallow negative values then just use a CHECK constraint. ———- Also, in general, it’s not enough to just add loads of distinct numeric types to a language or library; in fact that’s probably the wrong thing to do as it burdens the programmer with having to make (often hair-splitting) decisions ahead-of-time - instead I’d like the language to have me set-up high-level constraints/invariants on a numeric parameter, local, or field and then the compiler chooses the best low-level implementation based on those constraints (and constraint-inference a-la Hindley–Milner would be nice too)
- zX41ZdbW 4y agoClickHouse has support for unsigned integer types: UInt8, UInt16, UInt32, UInt64, UInt128, and UInt256. The main applications are: hashes and identifiers, e.g., visitor identifier in clickstream.
- fisf 4y agoJava pretty much provides BigDecimal for that
- pclmulqdq 4y agoI'm increasingly convinced that peoples' problems with 754 types are psychological. With integers, they are very careful about order of operations, precision, rounding, etc. With doubles, the average programmer fires and forgets. Sometimes that has bad consequences. A decimal type is definitely necessary, and you can use IEEE decimal floating point for that, too! Other decimal types are very useful. Rationals are a different story: rational numbers are great for addition and subtraction, but if you're going to be doing a lot of multiplication and division, you are going to find yourself also doing a lot of slow gcd calculations to reduce/renormalize the fractions (incidentally, decimal types also need renormalization, but it's a lot cheaper). Most cases where rational types are used today are very careful about not having long chains of these operations. As to NaNs: those are all considered pretty carefully, and 754-2019 actually reduced the amount of NaNs that propagate around quite a bit. For example, make sure you are using fmax(a, b) instead of std::max<double>(a, b).
- bawolff 4y ago> It's 2023 This is a game originally from 1999. Although i doubt it would make a difference.
- carlmr 4y ago>It's 2023: I'm still surprised that so many new languages/platforms still only provide only basic integer and IEEE-754 types for numerics: Rust, C#, Java, Swift and others still lack built-in, runtime-provided, nor even standard-library-provided rational types C# has the base-10 high accuracy decimal type: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types#characteristics-of-the-floating-point-types https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
- colejohnson66 4y agoThere are two problems with the `decimal` type: it's not IEEE-754 compliant, and it is 16 bytes, which prevents atomic reads/writes (without locks). This is a problem in one of my company's applications, as we have a background thread doing work entirely with `decimal` objects, but providing a readout for the GUI necessitates a lock on every write (even if never read), and another for the read by the GUI. There's barely any overhead (nanoseconds worth), but the logic to work around it is a pain, and, if you forget to actually use a lock, good luck with that bug. .NET 8, however, will introduce[0][1] proper IEEE-754 decimal float types, including `Decimal32` and `Decimal64` which will allow atomic reads/writes. [0]: https://github.com/dotnet/runtime/issues/81376 https://github.com/dotnet/runtime/issues/81376 [1]: https://github.com/dotnet/runtime/issues/79004 https://github.com/dotnet/runtime/issues/79004
- ouid 4y agowhy would rationals be good for RTS. If all of your operations occur with some reasonable minimum denominator, just use ints. If not, that arithmetic is going to become unbounded really fast.
- samtho 4y ago> On a related note, can we agree that an application-programming language today should also include signed-and-unsigned "money-safe" decimal types I’m willing to bet that this has to do with how we historically have handled monetary values which is storing and computing it in the respective currency’s fractional unit. The advantage is that you can still do math operations directly on this value (as opposed to an object that contains it) and at most you’ll be off by, in the case of the US dollar, one cent. Because we cannot represent fractional units of the already smallest unit of currency, we have to choose a value anyway to charge or dispense. Unlike, say, if we stored it in dollars as a floating point and we can start compounding errors from the floating point type. What interesting is that pre computers, we didn’t even treat currency values as an real decimal (for base 10 currencies) and the decimal point was simply a convenient way to store partial values. I note this because you don’t see old ledgers where they store more than 2 decimal places, therefore IMO, this was just an integer in disguise all along. Old habits die hard?
- hermitdev 4y ago> Because we cannot represent fractional units of the already smallest unit of currency, we have to choose a value anyway to charge or dispense. Unlike, say, if we stored it in dollars as a floating point and we can start compounding errors from the floating point type. I've spent 20 years mostly working in finance. You'd be surprised at the prevalence of floating points used to represent currency. I cringe every time I see it, but it's surprisingly common (and wrong). More correctly, pricing of securities (from exchanges) is done with integers and a scaling factor. The factor is typically static and doesn't need to be transmitted on every tick. The factor tells you effectively how many fractional digits are present (e.g. the actual price is multiplied by 10^-factor). Factors of 4 or even 6 are somewhat common, but some securities have to go with less precision. I remember in particular Berkshire Hathaway causing issues with overflow using 32-bit ints in the last decade (because they never split their shares, the share price is quite large).
- wruza 4y agoYou'd be surprised at the prevalence of floating points used to represent currency. I cringe every time We all cringe but then there’s a “floating point or gtfo” ultimatum that most languages present to you. People would be happy to not use FP. But the reality is, you can have a monetary column in a 30 years old database engine, but not in a 5 years old language runtime. When you only have a hammer…
- Someone 4y ago> It's 2023: I'm still surprised that so many new languages/platforms […] still lack built-in, runtime-provided, nor even standard-library-provided rational types Because numerators and denominators can blow up easily, rational types effectively require storing them as bigints. That makes them bad from a performance viewpoint. Assuming your application will work fine with fixed-size rationals (which, IMO, is highly unlikely), the natural way to store rationals in fixed-size storage wastes lots of room. For example, in a (int32 nominator, int32 denominator) pair, there are about 2³² ways to encode ‘1’ that way, 2³¹ to encode ½, etc. I also think such a fixed-size rational type isn’t very natural to work with Rationals also require regularly computing a gcd when you add (1/3 + 1/6 isn’t 9/18 but 1/2) or multiply (10/21 × 7/5 isn’t 70/105 but 2/3) them, slowing down things more (you can use heuristics to avoid some of those simplifications, but not doing one when that’s possible may mean your rationals keep huge numerators and denominators, slowing down operations)
- kortex 4y agoI'm (vaguely) surprised there hasn't been a hardware type for rationals. Then the GCD operation could be done in hardware and everything would be fast (at least as fast as floats). Not surprising, because adding new hardware data types is hard, but the financial institutions would benefit from something like that. A rat64 could be: - 1 sign bit - 3 format bits. Determines where the decimal is located, e.g. 30.30 vs 52.8. - 60 bits of numerator and denominator. The split is determined by the format bit. 50.10 would be handy for any percent/ppt operations (e.g. Most USD operations) Might need an error bit, but that's my 5 minute gist.
- Someone 4y ago> Then the GCD operation could be done in hardware and everything would be fast (at least as fast as floats). Could it? Is there a fast way to do GCD in hardware? Googling gave me https://en.wikipedia.org/wiki/Binary_GCD_algorithm https://en.wikipedia.org/wiki/Binary_GCD_algorithm, which needs O(log₂(max(u, v))) operations to compute gcd(u,v), but I wouldn’t know whether that’s the best we can do in hardware, or whether that’s ‘as fast as floats’. Also, I don’t see how your format describes a rational type. Rationals store numbers as p/q for integer p and q, not with a decimal point.
- marcellus23 4y agoFoundation has the Decimal type (not technically part of the Swift standard lib, but in practice available for most uses of Swift).
- porkbrain 4y agoA related discussion about number types: The Great Debate @ARITH23: John Gustafson and William Kahan https://www.youtube.com/watch?v=LZAeZBVAzVw https://www.youtube.com/watch?v=LZAeZBVAzVw Kahan points out disadvantages of interval maths.
- gizmo 4y agoYou mean fixed point numbers presumably, because at the time using integers that use a variable amount of memory was totally out of the question. Today it’s still terrible for performance because you can’t use caches and simd effectively when numbers can’t be packed directly in arrays.
- zelphirkalt 4y agoDoes AoE2 do that? Packing numbers directly into arrays? Or does it rather deal with objects, which are variable size anyway? I am not sure how much high performance work has been done in AoE2. Maybe not that much.
- CodesInChaos 4y agoThe big problem with rationals is that it's easy to end up with both nominator and denominator growing without bounds, which IMO is an even bigger pitfall than those of floating-point or fixed-point.
- ilyt 4y ago> Not sure in what case it makes sense to have dying at smaller than 1. To the user all the numbers are integers, so why not use rationals instead of floats? If killing unit takes X attacks, user buys 10% attack damage upgrade, and it still takes X attacks they will be annoyed. If rounding down takes that to x-1 attacks it might be preferable. That being said you're absolutely right. just use HP*100 in calculations and display HP/100 to the user, and when you need to round into specific direction do it explicitly
- perihelions 4y agoRationals fail in their own spectacular ways. Take a simple approximate exponential decay: x ← (99/100) * x. In exact rational arithmetic, your program will happily calculate integers (99^k) and (100^k) for unboundedly large values of k (there's no reduction – they're all coprime), grinding the program to a halt until it runs out of memory. "Oh, the solution is obvious: you should simply round that to an approximate fraction..." — yes, and that's floating-point arithmetic. Point is, you can't use either with thinking, about numerical representations and their possible failure modes.
- zelphirkalt 4y agoThe difference is, that with rationals you do not lose any precision on the way. I doubt, that 99^k is part of AoE2 code. For the simple calculations in AoE2 regarding unit health, rationals will do a fine job. With rationals the rounding does not happen during the precise calculation. It happens explicitly, only once, at the end of the calculation, at the programmer's wish and conscious decision, thereby avoiding the kind of issue we see here. Rationals might not be the solve all problems type, but here they seem very applicable.
- perihelions 4y agoIt's a very common pattern, in its more general forms. Sure, you can abstract away the naive re-implementation of exp() loop { x ← a*x } as one function call x(t) = exp(-k*t), so that one looks pointless. But moving-average [0] type expressions loop { x ← a*x + f(t) } are more interesting. You can't generally refactor those into exp() calls, like x(t) = sum[ f(t_i) * exp(-k*(t - t_i)) ], since that introduces a memory leak – you need to hold an explicit list of all the past values of f(t), in order to compute it this way. That's why the pattern [0] is uniquely useful: it's a trick to incrementally accumulate a moving average using just one variable of state. [0] https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average https://en.wikipedia.org/wiki/Moving_average#Exponential_mov... Here's one uncontrived way this pattern might show up: "the NPC has a numeric disposition towards the player, and the player's actions have positive and negative effects on it. These effects have finite duration, and disposition decays back to the neutral value". Another way: you have a large list (like a time series) you want to reduce to a small one by interpolation. Another way: you have some PID-like controller in your program, and to make it behave, you need a cheap filter to clean a jittery input signal. (I'm building these for my Factorio factories right now – it's the only reason this example occurred to me. "x ← (99/100) * x" is a factory control component I had to implement out of int32_t's).
- tialaramex 4y agoThe trouble you have is that you really wanted reals and the rationals are not a much better substitute than the floats (which are mostly just one particularly specialised flavour of rational) It doesn't feel immediately like there ought to be a problem, but witness, mathematically almost all of the reals are Normal and unfortunately Normal doesn't mean "normal" it's a technical mathematical word, that for our purposes means roughly "completely batshit". Normal numbers are non-computable, so we definitely don't want to try to use them in software, which is a problem because again, almost all of the reals are Normal.
- l__l 4y agoHuh? This isn't making a lot of sense to me. Why do you say the rationals aren't much better then floats? In situations like this surely we absolutely want to be working in the rationals, and they're a very easy way to do these calculations correctly without having to worry about tolerance bounds for arbitrary-precision float calculations
- hgsgm 4y agoAlgebraics plus pi avoid almost all the concern you mention, but still have the same problem for software. All those nasty reals are unreachable by your program. The actual problems with irrationals is that it is extremely hard to measure their size without converting to rational approximation, so you are back to the original problem.
- still_grokking 4y ago> The more I write code, the more I notice how rarely one truly should want floats in most kinds of programs and how they almost always carry a whole bag of problems with them. This! Imho you more or less never want floats under "normal" conditions. Also in most application domains it makes exactly no difference that floats have HW support, as the bottlenecks are there usually elsewhere. And in case you really need real fast floating point math (say for simulations) you would do it anyway on the GPU nowadays. The only langue I know of that is sane in this regard is Pyret: https://www.pyret.org/ https://www.pyret.org/ [CTRL-F: Pyret has numbers] I'm still baffled no other new language thought about something so basic like number ever again. Everybody is just using what the HW provides natively since the year of yore. As a result there is no motivation for HW vendors to update the status quo form the state of the art of the 60's of the last century. Imho having support for rationals and (fixpoint) decimal numbers in std. hardware is long overdue! But it would only happen if there would be serous demand form the runtime / language vendors. Classical chicken egg problem.
- marcosdumay 4y agoFloats are a computer representation of scientific notation, used for scientific computation. Coincidentally, hardware support is extremely important for scientific computation. (A bit less now than in the 90's but still a showstopper.) Hardware support is also extremely important for games, but floats are not a good representation for most of it. IMO, game data should be composed exclusively of integral numbers, but it is natural to want a little bit more of precision some times.
- still_grokking 4y ago> Floats are a computer representation of scientific notation No, they aren't. They are IEEE 754 binary floating point data. Something extremely weird, without precedent outside of computers science. > Coincidentally, hardware support is extremely important for scientific computation. Maybe it was once, but today it isn't. If you need to do any serious number crunching you use nowadays dedicated hardware for that (which isn't part of the main CPU usually). (You could use CPU integrated GPUs or FPGAs for that, sure, but the point is that the FP unit on the CPU is usually way to slow for anything more serious.) > Hardware support is also extremely important for games That's more or less the only valid usage in mainstream left by now. But even there: > but floats are not a good representation for most of it. Exactly! As said, you could and should use ints for most things. For the rest you want actually rationals. Only in some very special circumstances floats are a good enough approximation. But the cases where this is true are mostly related to rendering, as it doesn't matter if some pixels shown for the fraction of a second have a marginally wrong color or are marginally off. But rendering is done on the GPU anyway. So again no reason to use FP features on the CPU. Even games using vector and quaternion math extensively this is just lib code in the engine. So there is no real reason this couldn't be moved to the GPU also, given an adequate architecture of the game engine. Such an approach brings even amazing possibilities for games; just have a look at for example https://store.steampowered.com/app/1468720/Ultimate_Epic_Battle_Simulator_2/ https://store.steampowered.com/app/1468720/Ultimate_Epic_Bat... Want to animate 10 million of individual NPCs? No problem, if you do it on the GPU! So imho the case for FP on the CPU is very shallow. But the need for rationals and decimals is a real thing. That's what the average cooperate developer needs day to day. It's a shame HW is stuck in the past since decades and there is still no promise of progress on the horizon. (Maybe FPGA accelerators build into CPUs will change that at some point. But we still don't have that, even it's overdue.)
- avgcorrection 4y ago> Seems all that could have been avoided by using rationals in general Why can’t they store HP internally as “millihealth” integers, do integer arithmetic (including division), and then divide that by 1000 purely for display?
- firstlink 4y agoThis would require competent senior programmers who are way too expensive, and anyways once hired are used for bullshit tasks like project management.
- fendy3002 4y agoIn terms of hp without decimal in gaming and related with max hp changing, the best solution should be to rounding down (or nearest zero rounding) the result, and keep minimum hp to 1 when max hp changes. In case for minimum hp unit (1 in this case) to have changed into lower max hp then they'll die too, hence why minimum 1 hp as the result is logical. This is exactly why heroes won't die in Dota2 when toggling off armlet while having 1 hp
- wruza 4y agoTbh, the only practical non-integers we all need (apart from science-ish pretence and graphics) are .00, .000, .0000 and .000000 decimals (money, qty, percent.00, milliqty). Literally all business and in-game accounting needs get covered by these. Even edge/rounding issues are irrelevant, because everyone is ready to just agree to either way of doing it, and that agreement is much more important than the method itself. This floating bullshit is basically of none, zero, nil practical use outside of academics. Modern CPUs can effectively emulate whole subroutines in microcode, but somehow we got stuck with that useless binary floating point standard for numbers. And on top of that, languages provide them as a default numeric system for all new software.
- camgunz 4y agoI dug into this problem a while ago and came to the conclusion that these are all rounding problems, which you still can't escape with arbitrary precision systems, because memory is finite. You're always making a tradeoff between precision and space, and you have to rely on programmers to make that tradeoff themselves. Enter the typical floating point types that do some of that work already: float/double. These let you not think (as much) about the memory your math might take up, but they still need you to think about rounding, which almost no one does. This is a long winded way of saying that if you set up your arbitrary precision arithmetic library to use 4 bytes and the same rounding mode (the default is round to nearest) as was used here, you'd reproduce the bug. You'd also reproduce the 64-bit bug moving up sizes, and the 128/256/etc bug. You gotta round. The good news, though, is that actually solves it! You can even use floats for money!
- gabereiser 4y agobool is_alive(const Entity* unit) { return unit->hp > ZERO_FLOAT; } Super easy… Likewise I’m with you, the more I code, the more I see these kinds of things as bad design choices. If you’re going to deal with a type, keep it that type. If you’re going to convert, you need to take care of edge cases like OP. And don’t just add 0.0001f; that’s an exploit waiting to happen by someone with a macro to pickup/drop relics.