Skip to content
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

Update the double/float formatters to return the shortest roundtrippable string. #22040

Merged
merged 15 commits into from Feb 1, 2019
Merged

Update the double/float formatters to return the shortest roundtrippable string. #22040

merged 15 commits into from Feb 1, 2019

Conversation

@tannergooding
Copy link
Member

@tannergooding tannergooding commented Jan 17, 2019

The double/float formatters are currently implemented using the Grisu3 and Dragon4 algorithms. However, they were only using the variants that return an explicitly provided digit count (precision).

This updates the algorithms to also support the variants that return a "shortest roundtrippable string" (i.e. the shortest string that, when reparsed, will return the original value). This variant is chosen for "R" and when no precision specifier is given.

This allows us to return strings that are both "pretty" and that will return the original value when requested.

This resolves:

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

This is marked [WIP] as I am still doing some final validation that everything works correctly.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

This passes all 100,000,000 of the ES6 validation tests.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

cc. @jkotas, @danmosemsft

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

Some examples:

1.1 (no change from previous):

R: 1.1
G: 1.1
G17: 1.1000000000000001

double.MaxValue:

R: 1.7976931348623157E+308
G: 1.7976931348623157E+308 (previously 1.79769313486232E+308)
G17: 1.7976931348623157E+308

0.84551240822557006:

R: 0.8455124082255701 (previously 0.84551240822557, which roundtrips to 0.84551240822556994)
G: 0.8455124082255701 (previously 0.84551240822557, which roundtrips to 0.84551240822556994)
G17: 0.84551240822557006

Loading

@danmoseley
Copy link
Member

@danmoseley danmoseley commented Jan 17, 2019

Do you aim to also fix G17: 1.1000000000000001 ?

Are you doing perf measurements?

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

Do you aim to also fix G17: 1.1000000000000001 ?

There is nothing to fix here, the user explicitly requested 17 digits.

Are you doing perf measurements?

Yes, I plan on getting some perf measurements here.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

Rebased onto dotnet/master

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 17, 2019

Going through the CoreFX failures now. There are 206 of them, but most of them look to be bugs that have been resolved.

For example: Microsoft.VisualBasic.Tests.ConversionsTests.ToSingle_Obejct_ReturnsExpected

The input is -2147483648 and the expected result is -2.147484E+09.

The input, when converted to a float, is exactly -2147483650 (0xCF000000). The expected result, when parsed, actually results in -2147483900 (0xCF000001).

This PR causes the result to be -2.1474836E+09, which roundtrips to the expected value of -2147483650 (0xCF000000).

  • This new string is also "better" (shorter) than the previous string returned by R, which was -2.14748365E+09
  • This new string is longer (by one digit) than the previous string returned by G, but it is now correct.

Loading

THIRD-PARTY-NOTICES.TXT Outdated Show resolved Hide resolved
Loading
@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 24, 2019

Rebased and rerunning tests as the old jobs were since deleted.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 25, 2019

Added a new commit which ensures nMaxDigits is set appropriately:

  • digits=0 was only "invalid" for G, where it was treated the same as -1 (for others, it just removes digits after the decimal point).
  • For G and R when returning the shortest roundtrippable string, we need to ensure that nMaxDigits picks the higher of number.DigitsCount or Double/SinglePrecision, this ensures that numbers like -60 are printed as -60, rather than as -6E+01.
  • For all format specifies other than G and R, precision specifies the number of digits after the decimal point which should be printed. We need to ensure that we always request at least Double/SinglePrecision digits in these cases to ensure that we don't accidentally cut off digits in the fractional part.
    • Ideally we would actually always ensure that Grisu3/Dragon4 completely fills the integral portion and then also fills the fractional portion to the requested number of digits. This is possible, but it requires some additional work.

I tested the following values: -60, 1.1, double.Epsilon, double.MaxValue, 0.84551240822557006, Math.PI, and Math.E
with the following format specifiers: none, C, F, N, E, G, P, R; where each format specifier was tested standalone and with precisions of 0-19 (inclusive).

The results are here:

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 25, 2019

double.MaxValue is the only one, from the values tested, where the precision specifier shows how it impacts both the integral and fractional portion (bullet point 3 in the above). The fix (to always fill the integral portion for these specifiers) should be fairly straightforward, but we should determine that doing so is desirable.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

The latest commit (which is hopefully the last one, outside disabling the CoreFX tests in CoreFX.issues.json) fixes the formatters to take the format specifier into account when handling the precision.

As a basica summary, this means G and R (which are requesting a number of significant digits) continue behaving the same, but C, E, F, N, and P (which are requesting a number of decimal digits) now take that into account.

In the latter case, this means that the trailing digit (when more than 17 digits are requested) is no longer always 0, it also means that we always fill the integral portion and only use precision for the digits after the decimal point. Some examples are:

- C17  $1.10000000000000010
+ C17  $1.10000000000000009
- C18  $1.100000000000000090
+ C18  $1.100000000000000089
- C19  $1.1000000000000000890
+ C19  $1.1000000000000000888
...
- E17  4.94065645841246540E-324
+ E17  4.94065645841246544E-324
- E18  4.940656458412465440E-324
+ E18  4.940656458412465442E-324
-E19  4.9406564584124654420E-324
+ E19  4.9406564584124654418E-324
...
- F    179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.00
+ F    179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.00
- F0   179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ F0   179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368
- F1   179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0
+ F1   179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0
...
- N17  123,456,789.01234600000000000
+ N17  123,456,789.01234567165374756
- N18  123,456,789.012346000000000000
+ N18  123,456,789.012345671653747559
- N19  123,456,789.0123460000000000000
+ N19  123,456,789.0123456716537475586

Loading

@jkotas
Copy link
Member

@jkotas jkotas commented Jan 26, 2019

this means that the trailing digit (when more than 17 digits are requested) is no longer always 0

Why can't we make them always 0?

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

Why can't we make them always 0?

The user requested x digits after the decimal point and rather than giving them x digits after the decimal point, we would give them x significant digits (regardless of whether they appeared before or after the decimal point). This resulted in weird (and IMO incorrect) behavior for numbers that had more than 15-17 significant digits. The fix to always return the requested number of decimal digits was only a few lines of additional code (basically just adjusting where the cutoff point was).

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

This should all line up with what is documented and given in examples here: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings. They just don't use any numbers that have more than 15-17 significant digits in their examples.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

The diff file (showing a few example numbers) from the latest change is here: new_diff.txt. The baseline remains the same.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

The latest changes brings the CoreFX failure count from 206 down to 55. I have gone through all 55 tests and they are all instances where either:

  • We were returning a string that was too short so the number didn't roundtrip and now we are returning a slightly longer string that does (no more than 9 digits total for float and 17 digits for double), or
  • We were returning a string that did roundtrip, but there was also a shorter string that did the same, in which case we now return the latter.

An example of the first case is for 18446744073709551616, where:

We were returning:    double = 1.84467440737096E+19;   float = 1.844674E+19
We are now returning: double = 1.8446744073709552E+19; float = 1.8446744E+19

An example of the second case is for Epsilon, where:

We were returning:    double=4.94065645841247E-324; float=1.401298E-45
We are now returning: double=5E-324;                float=1E-45

Another example of the second case is for float -3.40282347E+38, where:

We were returning:    -3.40282347E+38
We are now returning: -3.4028235E+38

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 26, 2019

There look to be a couple of asserts being hit in the Checked jobs as well (around LogBase2). I am looking into those.

Loading

@tannergooding tannergooding changed the title [WIP] Update the double/float formatters to return the shortest roundtrippable string. Update the double/float formatters to return the shortest roundtrippable string. Jan 28, 2019
@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 28, 2019

This should be ready for review now. Perf tests are running and I should have numbers shortly.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 29, 2019

Perf numbers here, with those for Double/Single being the generally applicable tests to look at:

It can basically be summed as:

  • In cases where we were already returning a "correct/roundtrippable" result, we show consistent or faster perf numbers.
    • For something like 1.0, we have a 10% perf gain.
    • For something like double.Epsilon we have a 41% perf gain.
  • In cases where we were returning an incorrect result (the string did not contain enough digits and did not roundtrip), we are now slightly slower and doing more work.
    • This looks to be fairly consistent around 30%. The default precision was originally 15 and, in these cases we are computing up to 17 digits (2 additional digits) instead.
    • This does impact some common numbers like Math.PI and Math.E (which would not roundtrip for double.Parse(value.ToString()), but now do)
  • The R specifier is faster across the board.
    • The largest win was 85% on `double.Epsilon (we get the right result the first time around)
    • The smallest win was 24% for 1 (we no longer have to test the value roundtrips)

I am also running a bench locally on the entire float input range to see what the average gains/losses are (as the perf tests above are only testing some specific common/edge case values).

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 29, 2019

Looks like, overall, the change (for ToString() and ToString("G")) is a regression. Testing all positive values Single values between 0 and PositiveInfinity (2139095040 values) for both double and float shows about a 30% perf regression (on average).

Some additional data would be that, before this PR, we only had 692689950 (32%) of the float values and 163779870 (8%) of doubles producing a round-trippable result. Noting that this covers the entire single-precision range, but only a fraction of the double-precision range.

After the change, by default, we are now producing roundtrippable results for 100% of the inputs and are producing the shortest string that allows this (which generally results in "prettier" results).

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 29, 2019

Given that we explicitly have the R specifier, I could change G back to always produce a 15-digit string.

However, doing so would mean some of the bugs listed in the original post would not be closed (or would only be partially resolved). One such case is https://github.com/dotnet/coreclr/issues/13615, which tracks double.Parse(double.MinValue.ToString()) and double.Parse(double.MaxValue.ToString()) failing.

I would like to hear some other opinions here as well, but my preference would be to take the perf hit here, and work at getting it back in other ways. Such as:

  • Grisu3 currently fails for some numbers, meaning they take additional time to compute the correct result (via the Dragon4 algorithm).
  • There were some possible wins in the Number.BigInteger struct making use of intrinsics

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 29, 2019

It is also worth noting that, for what I would presume is the most common case (serialization) there should be no perf regression. For code using R, their code is now significantly faster. For code that was working around the round-tripping bugs by explicitly requesting G17, there perf is the same (as they continue getting the same 17 digits).

  • I am working on getting more concrete numbers here, for the perf difference between G17 and R; as I expect it is on average faster (given that some numbers now do less work for a still roundtrippable result).

Edit: R is on average 40% faster across the board.
Edit: The explicit G9 (for single) and G17 (for double) is on average 20% faster across the board.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 30, 2019

CC. @danmosemsft, @stephentoub, @jkotas. Could I get some weigh-in here.

The summary of the above is:

  • For ToString() and ToString("G") we are 30% slower on average (for values in the single-precision input domain).
    • This is because we are now always returning the shortest roundtrippable result.
    • Previously only 32% of floats and 8% of doubles, in the tested domain, were returning a round-trippable result
  • For ToString("R") we are 40% faster on average (for values in the single-precision input domain).
    • This is because we no longer have to test that the value roundtrips at G15 and fallback to G17 if it fails
  • For float.ToString("G9") and double.ToString("G17") we are now 20% faster on average
    • This is because we now terminate at the requested number of digits
    • This is also because float has an explicit code-path that appropriately sets the bounds (thereby reducing the number of failures in Grisu3)

It is my belief that ToString("R"), ToString("G9"), and ToString("G17") are the "common" case, as these are the format specifiers we recommend (and we ourselves use) for serialization purposes. The perf loss on ToString() and ToString("G"), while unfortunate, will likely not be a problem in real-world code. We should also be able to win this back by taking some optimizations in the Number.BigInteger ref struct, such as using existing intrinsics and adding intrinsics for AddWithCarry and Mul128 (which cut down work in many of the core methods to a single instruction).

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 30, 2019

The alternative is to keep ToString() and ToString("G") as equivalent to ToString("G15") (what it does today). This would remove the perf regression for this scenario but would cause double.Parse(value.ToString()) to no longer roundtrip for the majority of values (for which we have several open bugs) and would not return the "shortest roundtrippable string" (which results in prettier results).

Loading

@jkotas
Copy link
Member

@jkotas jkotas commented Jan 30, 2019

The different performance characteristics look fine to me. Thanks for collecting the data.

I would be more worried about the compatibility / breaking potential of this change. I think we are ok on this front too, pending further feedback.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 31, 2019

@eerhardt, @ahsonkhan. I believe I have responded to all feedback (either with an appropriate fix or a comment explaining the reasoning).

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Feb 1, 2019

@ahsonkhan, any other feedback here?

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Feb 1, 2019

Logged https://github.com/dotnet/coreclr/issues/22343 to track the potential perf improvements to the Number.BigInteger ref struct.

Will hold off on merging until after I get a PR for the CoreFX test fixes up.

Loading

@ahsonkhan
Copy link
Member

@ahsonkhan ahsonkhan commented Feb 1, 2019

Will hold off on merging until after I get a PR for the CoreFX test fixes up.

In that case, marking as no merge.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Feb 1, 2019

CoreFX test fixes are here: dotnet/corefx#35016

Will merge this after I see the NetFX leg pass, the NetCore tests passed locally.

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 16, 2020

The implementation of this was a terrible decision, especially for those migrating from full framework to .net core. Not everyone cares about rounding tripping. Some of us actually care more about, I don't know, how things get displayed to users (.net isn't just used as a backend). Changing the default behavior and then making no way to easily override it was just very poor foresight.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 16, 2020

The changes are detailed here, including several ways to get output that is generally compatible with the previous formatting behavior: https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/#potential-impact-to-existing-code

However, to give some perspective on why this was done...

Imagine you were working with integers rather than floats. That by default, when you called int.ToString() it converted 1, 2, 3, and 4 all to "1", 5, 6, 7, and 8 to "5", etc. You had an alternative you could use int.ToString("R") and it generally did the right thing 1 to "1", 2 to "2"; but every now and again it might return something that was off by one (e.g. 17 got returned as "16"). Then, along with everything else, there were bugs in the parser which meant that even if you provided "1234" as the input string, you might not get back 1234 as the result (or, in the case of floating-point the closest representable value).

This was the world of float/double formatting/parsing prior to the fixes and while it didn't cause nearly as many problems as it would have for int (namely due to float/double generally being viewed as approximations already). It did lead to numerous customer issues and other subtle bugs that could creep into programs. It also meant we were not compatible with most programming languages (including with how C# parsed floating-point literals) or compliant with the IEEE 754 specification (when these types are explicitly defined to be the IEEE 754 types, and have been for 20 years).

Breaking changes are certainly frustrating, but so is not being able to write a program that can deterministically compute the correct result or that can share information losslessly with other programs. The same goes for favoring display usage over correctness as the default behavior (however, I think we hit a good middle ground by returning the shortest roundtrippable string). This along with the changes required for users to continue printing "pretty" strings after the break being fairly minimal tipped the scales in favor of making the break to provide a better .NET.

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 16, 2020

the changes required for users to continue printing "pretty" strings after the break being fairly minimal

WHAT?!? that's nonsense. You're changing the default behavior of probably one of the most fundamental methods in the entire framework: double.ToString(). Which in any non-trivial program may be called from thousands of locations, many times implicitly. The only way to get the original behavior is to change EVERY SINGLE INDIVIDUAL CALL to ToString("G15"). That's a far cry from "fairly minimal". In many cases it may be quite impossible, because the ToString() call is implicit or down in some library you have no control over. Why on earth wasn't this some sort of "opt-in" thing?
e.g.
double.PreferRoundtrip = true

At the very least, make it easy to opt-out:
e.g.
double.UseLegacyFormatBehavior = true

If you wan't to fix bugs with round tripping, great, fix those bugs. But don't change the default behavior/intention of the parameter-less .ToString() from "give me a pretty string I can display to a user" to "give me an ugly string that will be great for serialization". There's already ToString("R") for that.

This is not "providing a better .NET". You've made it an order of magnitude worse.

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 16, 2020

Microsoft (R) Visual C# Interactive Compiler version 3.4.1-beta4-19610-02 ()
Loading context from 'CSharpInteractive.rsp'.
Type "#help" for more information.
> 1.2 + 2.4
3.5999999999999996 

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 16, 2020

It's worth noting that csi does not use the parameterless ToString() and instead forces results to be roundtrippable by default.

However, lets look at a simple program:

static void Main(string[] args)
{
    Test(1.2 + 2.4);
    Test(3.6);
}

static void Test(double input)
{
    Console.WriteLine($"ToString():    {input.ToString()}");
    Console.WriteLine($"ToString(G15): {input.ToString("G15")}");
    Console.WriteLine($"ToString(G17): {input.ToString("G17")}");
    Console.WriteLine($"ToString(G99): {input.ToString("G99")}");
    Console.WriteLine($"Raw Bits:      {BitConverter.DoubleToInt64Bits(input).ToString("X16")}");
    Console.WriteLine();
}

On .NET Full Framework and .NET Core Prior to 3.0:

ToString():    3.6
ToString(G15): 3.6
ToString(G17): 3.5999999999999996
ToString(G99): 3.5999999999999996
Raw Bits:      400CCCCCCCCCCCCC

ToString():    3.6
ToString(G15): 3.6
ToString(G17): 3.6000000000000001
ToString(G99): 3.6000000000000001
Raw Bits:      400CCCCCCCCCCCCD

On .NET Core 3.0 and later:

ToString():    3.5999999999999996
ToString(G15): 3.6
ToString(G17): 3.5999999999999996
ToString(G99): 3.5999999999999996447286321199499070644378662109375
ToString(R):   3.5999999999999996
Raw Bits:      400CCCCCCCCCCCCC

ToString():    3.6
ToString(G15): 3.6
ToString(G17): 3.6000000000000001
ToString(G99): 3.600000000000000088817841970012523233890533447265625
ToString(R):   3.6
Raw Bits:      400CCCCCCCCCCCCD

The value returned is different because the computed result is not actually 3.6, meaning 1.2 + 2.4 != 3.6. It can be important to differentiate these due to bugs that can otherwise occur:
image

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 16, 2020

Which explains why you should never use "==" to compare doubles, which I'm already aware of, but doesn't address the real issue. No non-programmer end user is ever going to be happy to see "3.5999999999999996".

You changed the fundamental purpose of ToString() from "displaying numbers" to "serializing numbers", without providing an escape hatch.

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 16, 2020

Which explains why you should never use "==" to compare double

IEEE 754 floating-point arithmetic is deterministic. 1 + 1 will always equal 2 and 1.2 + 2.4 will always equal 3.5999999999999996. It is perfectly acceptable, in an IEEE 754 compliant environment, to use == for equality given a set of known inputs, their operations, and an expected output.

One issue people frequently encounter is that they try to treat it as normal arithmetic and expect things like a + (b + c) == (a + b) + c or that 0.3 is "exactly representable" because it contains less than 17 digits. This leads to cases where they expect that 1.2 + 2.4 must equal 3.6 when in fact the nearest representable result is just under (this is due to 1.2 and 2.4 both not being exactly representable; they are actually: 1.1999999999999999555910790149937383830547332763671875 and 2.399999999999999911182158029987476766109466552734375).

You changed the fundamental purpose of ToString() from "displaying numbers" to "serializing numbers"

All other primitive types produce roundtrippable numbers by default and users are expected to call ToString(format, formatProvider) if they want to customize the display (whether that is to produce culture agnostic output or to add commas or other separators). float/double are no different in that regard and it is frequently the case, especially when wanting to display "pretty" output that you don't want the previous default behavior of G15 either.

For example: double.Epsilon is an easily identifiable case where we return better output than previously:

Before: 4.94065645841247E-324
After:  5E-324

Loading

@tannergooding
Copy link
Member Author

@tannergooding tannergooding commented Jan 16, 2020

Another example is, for System.Single, a majority all results above 10000006 now have a "pretty" result printed (current, before, G9):

current   before        G9
10029114  1.002911E+07  10029114
10029115  1.002912E+07  10029115
10029116  1.002912E+07  10029116
10029117  1.002912E+07  10029117
10029118  1.002912E+07  10029118
10029119  1.002912E+07  10029119
10029120  1.002912E+07  10029120
10029121  1.002912E+07  10029121
10029122  1.002912E+07  10029122
10029123  1.002912E+07  10029123
10029124  1.002912E+07  10029124
10029125  1.002912E+07  10029125
10029126  1.002913E+07  10029126
10029127  1.002913E+07  10029127
10029128  1.002913E+07  10029128
10029129  1.002913E+07  10029129
10029130  1.002913E+07  10029130
10029131  1.002913E+07  10029131
10029132  1.002913E+07  10029132
10029133  1.002913E+07  10029133
10029134  1.002913E+07  10029134
10029135  1.002914E+07  10029135
10029136  1.002914E+07  10029136
10029137  1.002914E+07  10029137
10029138  1.002914E+07  10029138

There are similar cutoffs for System.Double (although I don't recall where that is off the top of my head).

Loading

@danmoseley
Copy link
Member

@danmoseley danmoseley commented Jan 16, 2020

@davidmilligan could you say a little more about what quantities you are using floats to represent, and why you're summing them? I assume these are continuous quantities like temperature or something of that sort?

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 17, 2020

Here's an example of the issue with a trivial WPF app: https://github.com/davidmilligan/RoundingRegressionExample

Type 1.2 into the first text box and 2.4 into the second one and click Add. A normal user will not be very happy with that result. The "fixed" box has the suggested fix of using G15 from the article, and granted, in this trivial example, applying the fix is very easy, and it works. However, imagine any sort of non-trivial project. Perhaps it has hundreds of text boxes bound to doubles across dozens of screens along with thousands of other text boxes not bound to doubles. Applying the fix is anything but easy, in fact its extremely time consuming (you can't do a find and replace or anything like that, you've got to check each and every Binding in your application, out of maybe 10 thousand, and see if it's binding to a double), and there's no way to just globally change the default behavior of ToString() back to what it used to be, and no generic way to get WPF to do something different either.

@tannergooding You keep trying to prove why it's better. I don't dispute that it probably is "better" in general, but I don't care. It's completely different and focused in a completely different direction now. That's my point. People have structured massive programs around the intentions of the previous behavior, and it's just such a basic, fundamental part of the framework, making this breaking change just doesn't make sense, at least not without a way to opt-out.

Loading

@danmoseley
Copy link
Member

@danmoseley danmoseley commented Jan 17, 2020

@davidmilligan I'm curious what kinds of quantities you're representing in your app with floats -- not how to repro what you're seeing. I'm not suggesting it's not a reasonable scenario, just curious what your scenario is for adding floats and presenting them.

Loading

@davidmilligan
Copy link

@davidmilligan davidmilligan commented Jan 17, 2020

In one particular scenario, these are weights in pounds. Users may be entering various weights of individual items that make up some larger components and the total weight is computed.

Loading

airbreather added a commit to NetTopologySuite/NetTopologySuite.IO.GPX that referenced this issue Jan 23, 2020
- Revert workarounds for the issues resolved by dotnet/coreclr#22040
- See that PR for the links to all the specific issues it resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants