I tried the equivalent of Michael Meadows EDIT 2, but in VB.NET and got a different result. (Specifically both the Double
and Decimal
results were 600000.0238418580.)
I have determined the difference is with the compile-time accuracy of a float
(Single
) division stored into a float
in C#, (which seems to be more equivalent to VB.NET's accuracy when storing into a Double
) and what happens (in both languages unsurprisingly) when you force the division to occur at runtime.
So, THREE_FIFTHS
and vTHREE_FIFTHS
provide different results for the asDouble
summation:
const int ONE_MILLION = 1000000;
float THREEsng = 3f;
float FIVEsng = 5f;
float vTHREE_FIFTHS = THREEsng / FIVEsng;
const float THREE_FIFTHS = 3f / 5f;
Console.WriteLine("Three Fifths: {0}", THREE_FIFTHS.ToString("F10"));
float asSingle = 0f;
double asDouble = 0d;
decimal asDecimal = 0M;
for (int i = 0; i < ONE_MILLION; i++)
{
asSingle += (float) THREE_FIFTHS;
asDouble += (double) THREE_FIFTHS;
asDecimal += (decimal) THREE_FIFTHS;
}
Console.WriteLine("Six Hundred Thousand: {0:F10}", THREE_FIFTHS * ONE_MILLION);
Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
Console.WriteLine("vThree Fifths: {0}", vTHREE_FIFTHS.ToString("F10"));
asSingle = 0f;
asDouble = 0d;
asDecimal = 0M;
for (int i = 0; i < ONE_MILLION; i++)
{
asSingle += (float) vTHREE_FIFTHS;
asDouble += (double) vTHREE_FIFTHS;
asDecimal += (decimal) vTHREE_FIFTHS;
}
Console.WriteLine("Six Hundred Thousand: {0:F10}", vTHREE_FIFTHS * ONE_MILLION);
Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
The result with the difference hightlighted is:
Three Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 599999.9999886850
Decimal: 600000.0000000000
vThree Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 600000.0238418580
Decimal: 600000.0000000000
My question is, can you get C# to get a const float
expression with the equivalent of the runtime (and VB.NET) result? (I.e. produce a THREE_FIFTHS
with the same results as vTHREE_FIFTHS
.)
THREE_FIFTHS
has the same value as vTHREE_FIFTHS
in your example (you can see this with BitConverter.GetBytes
). It only differs in the way it is added to a double in your code.
I think your difference is due to the way that the C# compiler handles const
s as if they were literals, in some ways. E.g. this operation, though it's normally not allowed without a cast, is ok because the const
lets the compiler see that the int
is small enough to work out:
const int i = 5;
byte b = i;
In your case, this means that it doesn't add the single-precision value 3/5 to the double, but calculates the double value 3/5 and adds that. Unfortunately this extra "intelligence" has a side effect. You can get around it by storing the const float
as a float
first, e.g.:
float f = THREE_FIFTHS;
asDouble += (double) f;
With this, both ways calculate the double as 600000.0238418580
.
You can see more detail on the weirdness with these outputs:
string GetByteString(double d)
{
return string.Join("", BitConverter.GetBytes(d).Select(b=>b.ToString("X2")));
}
string GetByteString(float f)
{
return string.Join("", BitConverter.GetBytes(f).Select(b=>b.ToString("X2")));
}
double vd = vTHREE_FIFTHS;
double d = THREE_FIFTHS;
const double cd = THREE_FIFTHS;
float f = THREE_FIFTHS;
const double cd2 = 3d / 5d;
double d2 = 3d / 5d;
double df = f;
// doubles
Console.WriteLine(GetByteString((double)THREE_FIFTHS));
Console.WriteLine(GetByteString(vd));
Console.WriteLine(GetByteString(df));
Console.WriteLine(GetByteString(d));
Console.WriteLine(GetByteString(cd));
Console.WriteLine(GetByteString(cd2));
Console.WriteLine(GetByteString(d2));
// floats
Console.WriteLine(GetByteString(f));
Console.WriteLine(GetByteString(vTHREE_FIFTHS));
Console.WriteLine(GetByteString(THREE_FIFTHS));
prints this:
333333333333E33F
000000403333E33F <-- these are the only ones that were actually
000000403333E33F <-- converted from 32-bit float values to doubles
333333333333E33F
333333333333E33F
333333333333E33F
333333333333E33F
9A99193F
9A99193F
9A99193F
It does look like the answer is "it can't be done", because, as Tim S. pointed out, the compile-time constant is not really constant, but actually reinterpreted at each use. Specifically the (double)
cast version of the constant produces a different result to the runtime result.
void Main()
{
const int ONE_MILLION = 1000000;
float THREEsng = 3f;
float FIVEsng = 5f;
float vTHREE_FIFTHS = THREEsng / FIVEsng;
const float THREE_FIFTHS = 3f / 5f;
Console.WriteLine("Three Fifths: {0}", THREE_FIFTHS.ToString("F10"));
float asSingle = 0f;
double asDouble = 0d;
decimal asDecimal = 0M;
for (int i = 0; i < ONE_MILLION; i++)
{
asSingle += (float) THREE_FIFTHS;
asDouble += (double) THREE_FIFTHS;
asDecimal += (decimal) THREE_FIFTHS;
}
Console.WriteLine("Six Hundred Thousand: {0:F10}", THREE_FIFTHS * ONE_MILLION);
Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
Console.WriteLine(GetByteString((float) THREE_FIFTHS));
Console.WriteLine(GetByteString((double) THREE_FIFTHS));
Console.WriteLine(GetByteString((decimal) THREE_FIFTHS));
Console.WriteLine("vThree Fifths: {0}", vTHREE_FIFTHS.ToString("F10"));
asSingle = 0f;
asDouble = 0d;
asDecimal = 0M;
for (int i = 0; i < ONE_MILLION; i++)
{
asSingle += (float) vTHREE_FIFTHS;
asDouble += (double) vTHREE_FIFTHS;
asDecimal += (decimal) vTHREE_FIFTHS;
}
Console.WriteLine("Six Hundred Thousand: {0:F10}", vTHREE_FIFTHS * ONE_MILLION);
Console.WriteLine("Single: {0}", asSingle.ToString("F10"));
Console.WriteLine("Double: {0}", asDouble.ToString("F10"));
Console.WriteLine("Decimal: {0}", asDecimal.ToString("F10"));
Console.WriteLine(GetByteString((float) vTHREE_FIFTHS));
Console.WriteLine(GetByteString((double) vTHREE_FIFTHS));
Console.WriteLine(GetByteString((decimal) vTHREE_FIFTHS));
}
// Define other methods and classes here
string GetByteString(double d)
{
return "#" + string.Join("", BitConverter.GetBytes(d).Select(b=>b.ToString("X2")));
}
string GetByteString(decimal d)
{
return "D" + string.Join("", Decimal.GetBits(d).Select(b=>b.ToString("X8")));
}
string GetByteString(float f)
{
return "S" + string.Join("", BitConverter.GetBytes(f).Select(b=>b.ToString("X2")));
}
Output:
Three Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 599999.9999886850
Decimal: 600000.0000000000
S9A99193F
#333333333333E33F
D00000006000000000000000000010000
vThree Fifths: 0.6000000000
Six Hundred Thousand: 600000.0000000000
Single: 599093.4000000000
Double: 600000.0238418580
Decimal: 600000.0000000000
S9A99193F
#000000403333E33F
D00000006000000000000000000010000