# Friday, 28 February 2003
« Mauve | Main | Hello Mono (2) »
Undefined behavio[u]r

While fixing bugs in ikvm to get more Mauve tests working (BTW, current results: 224 of 7584 tests failed), I ran across a small but interesting difference between Java and C# (and the underlying bytecodes) in converting floating point numbers to integers.

Java code:

class Test {
  public static void main(String[] args) {
    float f = Integer.MIN_VALUE;
    f -= 5;
    System.out.println((
int)(f + 0.5f));
  }
}

Java output: -2147483648 (= Integer.MIN_VALUE)

C# code:

class Test {
  static void Main() {
    float f = int.MinValue;
    f -= 5;
    System.Console.WriteLine((
int)(f + 0.5f));
  }
}

C# output (release build): 2147483644
C# output (debug build): -2147483647

It turns out that the CIL instruction conv.i4 returns an undefined value when the float value lies outside of the range representable by an 32 bit integer, unlike the JVM's f2i instruction which is defined to return either Integer.MIN_VALUE or Integer.MAX_VALUE in that case.

If you want your .NET code to run consistently, use the conv.ovf.i4 instruction that checks for overflow. In C# this can be done by using a checked block:

class Test {
  static void Main() {
    float f = int.MinValue;
    f -= 5;
    checked {
      System.Console.WriteLine((
int)(f + 0.5f));
    }
  }
}

Now, instead of returning an undefined value, the cast throws a System.OverflowException.

The JVM designers felt it was very important not to have any undefined or implementation defined behavior. One of the lessons learned from C and C++ is that whenever there is undefined or implementation defined behavior, code will be written that depends on the behavior of a particular platform/compiler. The JVM designers wanted to removed this source of portability problems.

However, they payed a price in performance for this. A well known example is the case of floating point performance (on x86), which was later "fixed" by relaxing the floating point specification and introducing strictfp. As Morpheus would say: "Welcome to the real world!" (Don Box claims everything in computing can be understood by watching The Matrix enough times).

Let's examine the performance of Java's f2i compared with .NET's conv.i4. Please note that the usual disclaimer wrt (micro) benchmarking applies.

Here is the loop I used:

float f = SOME_VALUE;
for(int i = 0; i < 10000000; i++) {
  int j = (int)f;
  f = j;
}

Timings:

 

 

SOME_VALUE

 

 

0

+Infinity

Sun JDK 1.1 (Symantec JIT)

Float

600 ms

2000 ms

Double*

800 ms

2300 ms

Sun J2RE 1.4.1 (Hotspot Client VM)

Float

600 ms

3800 ms

Double*

1100 ms

2600 ms

.NET 1.0

Float

500 ms

500 ms

Double*

800 ms

800 ms

.NET 1.0

(checked)

Float

800 ms

n/a

Double*

1200 ms

n/a

* For the double test, the value was cast to a long instead of an int.

Let's look at the code that the Symantec JIT uses to convert a float to an int:

01F543F0  ftst             
01F543F2  fldcw       word ptr ds:[1F5FB30h]
01F543F8  push        eax 
01F543F9  fistp       dword ptr [esp]
01F543FC  fldcw       word ptr ds:[1F5FB34h]
01F54402  pop         eax 
01F54403  cmp         eax,80000000h
01F54408  je          01F5440B
01F5440A  ret             
01F5440B  push        eax 
01F5440C  fnstsw      ax  
01F5440E  sahf            
01F5440F  pop         eax 
01F54410  jp          01F54416
01F54412  adc         eax,0FFFFFFFFh
01F54415  ret             
01F54416  xor         eax,eax
01F54418  ret             

When the JIT compiles an f2i bytecode, it emits a call to this function. I've never written any x87 code, so I'm going to have to make this up as I go. Let's look at each individual instruction:

01F543F0  ftst 

No idea what the purpose of this is.

01F543F2  fldcw       word ptr ds:[1F5FB30h] 

Presumably this is used to mask the invalid-arithmetic-operand exception (#IA) that is generated by FISTP when it encounters a value that cannot be represented as a 32 bit integer.

01F543F8 push eax 

Make room on the stack for the integer.

01F543F9 fistp dword ptr [esp] 

Convert value on the floating stack to integer and store on the regular stack in slot we just created.

01F543FC fldcw word ptr ds:[1F5FB34h] 

Restore the FPU control word to its normal Java setting.

01F54402 pop eax 

Load the integer we just created into EAX.

01F54403 cmp eax,80000000h

Is it the integer indefinite value? When the #IA exception is masked FISTP returns the integer indefinite value for floats that cannot be represented as a 32 bit integer.

01F54408 je 01F5440B 

If it was the integer indefinite value, continue executing at 01F5440B.

01F5440A ret 

If not, return.

01F5440B push eax 

Save the integer indefinite value.

01F5440C fnstsw ax 

Load the FPU status flags in AX.

01F5440E sahf 

Load AH into the CPU status flags.

01F5440F pop eax 

Recover the integer indefinite value.

01F54410 jp 01F54416 

If the parity flag is set, the original float was a NaN, so we jump to the code that clears EAX and returns.

01F54412 adc eax,0FFFFFFFFh 

If the carry flag is set, leave EAX unchanged (i.e. add zero) otherwise add -1. This is a clever way of turning 0x80000000 into either 0x80000000 or 0x7FFFFFFF.

01F54415 ret 

Return to the caller.

01F54416 xor eax,eax 
01F54418 ret

Set EAX to zero and return to the caller.

After analyzing this code, it's kind of surprising that the "exceptional" case (when the float lies outside of the representable range) is so much slower.

Conclusion: As usual there is no conclusion, but hopefully we learned something today ;-)

Friday, 28 February 2003 11:56:00 (W. Europe Standard Time, UTC+01:00)  #    Comments [0]