Another great question was asked on the C# subreddit about integer vs. floating point division in C#. In the post, the commenter essentially asked why, if they store the result of their division in a float, does the whole calculation produce the “wrong” answer?
They had the following code.
int boardSize = 16;
float offset = ((boardSize % 2f) - 1) / -2;
Console.WriteLine(offset);
offset = ((boardSize % 2) - 1) / -2;
Console.WriteLine(offset);
The first version produced the correct answer, 0.5. The second produced an “incorrect” answer, 0. Why is this?
In order to run a calculation of any kind, you need to have consistent types in C#. In the first example, the boardSize % 2f
has a floating-point operand. This causes the whole expression to be converted to floating point, which gives the correct answer.
In the second version, the whole expression is made up of integers, so we get integer calculations the whole way. Just storing the result in a float will not force the whole statement to run using floating-point operations!
Let’s look at the code the compiler gives us.
{
int num = 16;
Console.WriteLine((float) (((double) num % 2.0 - 1.0) / -2.0));
Console.WriteLine((float) ((num % 2 - 1) / -2));
}
Check it out. The compiler automatically turns the first calculation into floating points for us. If we check the intermediate language produced, we verify this is what’s happened
//
// Load the value 16 into our local variable "num"
//
// IL_0001: ldc.i4.s 16 // 0x10
// IL_0003: stloc.0 // 'num [Range(Instruction(IL_0003 stloc.0)-Instruction(IL_0020 ldloc.0))]'
// IL_0004: ldloc.0 // 'num [Range(Instruction(IL_0003 stloc.0)-Instruction(IL_0020 ldloc.0))]'
//
// Here is where we convert the value at the top of our stack (num)
// to a float.
//
// IL_0005: conv.r4
//
// Then we push floating point 2 onto the stack...
//
// IL_0006: ldc.r4 2
//
// ...and divide the two, taking the remainder and pushing it
// onto the stack.
//
// IL_000b: rem
//
// Then we push floating point 1 onto the stack...
//
// IL_000c: ldc.r4 1
//
// ...and subtract 1 from the remainder we found earlier, pushing
// that value on the stack.
//
// IL_0011: sub
//
// Now we load floating point -2 onto the stack, and perform our
// division.
//
// IL_0012: ldc.r4 -2
// IL_0017: div
//
// In order to print the value to screen, we need to have a local
// variable that contains our result. The compiler generates
// that for us.
//
// IL_0018: stloc.1 // V_1
// IL_0019: ldloc.1 // V_1
// IL_001a: call void [System.Console]System.Console::WriteLine(float32)
// IL_001f: nop
//
//
// Now for the all-integer version. Check this out. In IL,
// there are dedicated instructions for loading some values
// onto the stack that are integers.
//
// First pull the value stored in num and put it on the stack.
//
// IL_0020: ldloc.0 // 'num [Range(Instruction(IL_0003 stloc.0)-Instruction(IL_0020 ldloc.0))]'
//
// Load integer 2 onto the stack, perform division, take remainder.
// This is all integer!
//
// IL_0021: ldc.i4.2
// IL_0022: rem
//
// Then load integer 1 onto the stack, perform subtraction.
//
// IL_0023: ldc.i4.1
// IL_0024: sub
//
// Finally our division.
//
// IL_0025: ldc.i4.s -2 // 0xfe
// IL_0027: div
//
// CHECK THIS OUT! In our original code, we created a variable
// called offset, type float. So to store our integer result
// in a floating point type, we have to cast our integer
// back to float.
//
// IL_0028: conv.r4
// IL_0029: stloc.1 // V_1
// IL_002a: ldloc.1 // V_1
// IL_002b: call void [System.Console]System.Console::WriteLine(float32)
// IL_0030: nop
// IL_0031: ret
//
{
int num = 16;
Console.WriteLine((float) (((double) num % 2.0 - 1.0) / -2.0));
Console.WriteLine((float) ((num % 2 - 1) / -2));
}
The moral of our story?
- Integer operations produce less code.
- As soon as one operand is a float, the whole operation turns into floating point. The compiler makes sure of that.
- Storing your result in a float doesn’t force the expression to evaluate using floating point.