IEEE 754 mode for Java

210 views
Skip to first unread message

Craig Mitchell

unread,
Apr 26, 2025, 7:24:01 PMApr 26
to GWT Users
I have some code that does millions of chained multiplications and divisions with doubles.

When I compile the code with GWT to Javascript, I get consistent results, no matter what the browser.  I believe this is because Javascript follows the IEEE 754 standard.

However, running the same code in Java does not get consistent results.  Not only are the results different from Javascript, they are different based on how I run the tests.  Ie: Running from IntelliJ's JUnit runner vs running with "mvn test" gives different results.

I would like the results from Java to match the results from JavaScript (follow the IEEE 754 standard).

I'm on Windows, running Java Temurin 21.0.5 64-Bit.

Has anyone else faced this problem?  Any ideas for a solution?

Colin Alworth

unread,
Apr 26, 2025, 7:44:32 PMApr 26
to noreply-spamdigest via GWT Users
(Sounds like this isn’t actually a gwt question at all, but a java one, since gwt apparently is “right” where Java is wrong?)

Can you give an example of the computation you’re doing - like a series of multiply/add/etc operations, or using Math/StrictMath and friends?

Also is there any chance that your tests are recognized by the compiler to be operations on constants, so the work is all being done at compile time rather than letting the browser actually do any work?

Which browsers do you test with - does HtmlUnit pass these as well?

-- 
  Colin Alworth

--
You received this message because you are subscribed to the Google Groups "GWT Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-web-tool...@googlegroups.com.

Craig Mitchell

unread,
Apr 26, 2025, 8:06:11 PMApr 26
to GWT Users
You are correct, not really a GWT question.  Apologies.  Just a by product of using GWT.


> Can you give an example of the computation you’re doing

This is my problem at the moment.  I haven't been able to replicate it in a simple example.  Trying something like:

double test = 12345678d;
for (int i = 1; i < 1_000_000; i++) {
  if (i % 2 == 0) {
    test = test * (double)i;
  } else {
    test = test / (double)i;
  }
}
Gives 15473.01664039943 in all cases.  In my real world test, there are thousands of lines of code.

> Also is there any chance that your tests are recognized by the compiler to be operations on constants, so the work is all being done at compile time rather than letting the browser actually do any work?

Obsolutely there is.  I asked AI about my problem, and it told me it was due to Java not following the IEEE 754 standard and I have to expect inconsistant results.  However, I don't know if that's correct.  There are people saying Java does follow IEEE 754 and others that say it doesn't.

> Which browsers do you test with - does HtmlUnit pass these as well?

I've checked Chrome, Edge, and Firefox.  All give the same result.  I haven't tried HtmlUnit yet (I'll implement some GWTTestCase's now).

Thomas Broyer

unread,
Apr 27, 2025, 8:39:06 AMApr 27
to GWT Users
On Sunday, April 27, 2025 at 2:06:11 AM UTC+2 ma...@craig-mitchell.com wrote:
I asked AI about my problem, and it told me it was due to Java not following the IEEE 754 standard and I have to expect inconsistant results.  However, I don't know if that's correct.  There are people saying Java does follow IEEE 754 and others that say it doesn't.

RobW

unread,
Apr 28, 2025, 3:01:33 AMApr 28
to GWT Users
If your solution relies on absolute accuracy across different languages and machines but you're using floating point I'd say you are setting yourself up for some major heartache and issues. Even with IEEE 754, and supposedly strict rules on rounding I wouldn't trust the fidelity across machines. Do you really need to do the math both on the server in Java and in the browser? Pick one and use that maybe. Or, if you need complete fidelity, use something other than floating point - integer arithmetic or maybe BigDecimal on the server.

Craig Mitchell

unread,
Apr 28, 2025, 4:18:02 AMApr 28
to GWT Users
If interested, my situation is a car racing game.  The graphics code is in the client module, and the logic code is in the shared module.  The player plays in the browser, and once finished, their input controls are sent to the server, that then replays the race in the logic code to make sure they didn't cheat.

Thus, the logic code is run in JavaScript in the browser, and in Java on the server.

In addition to that, players also race against replays of other players.  Those replays are generated in the browser, again using the logic code from the recorded player inputs.

JavaScript has been rock solid, across all browsers and platforms (PC / Mac / Android / iPhone / Smart TVs, Tesla browser / ...)  The logic code always outputs the same results.

Java not so much.

There is a pattern however:
- The failures always occur with races that have computer opponents (which is about 30% of the races).
- The failures always consistently fail with the same results.  Ie: If I run a test with mvn test, I'll always get one result.  If I run the same test with IntelliJ, I'll always get the same different result.

I'm trawling through the computer opponent logic code to see if I can see anything suspicious.

I have avoided obvious things like:
- Not using Math.sin, Math.cos, Math.atan, etc. as those return different results based on the platform.
- Not using floats, as they are not suported by JavaScript.

As Colin suggested, I'm also implementing the tests with GWTTestCase, to see if they pass or not.

The game if you're curious: https://drift.team/

Cheers.

Juan Pablo Gardella

unread,
Apr 28, 2025, 7:47:36 AMApr 28
to google-we...@googlegroups.com
Amazing game! Congratulations

--
You received this message because you are subscribed to the Google Groups "GWT Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-web-tool...@googlegroups.com.

Jens

unread,
Apr 28, 2025, 11:06:27 AMApr 28
to GWT Users
- The failures always occur with races that have computer opponents (which is about 30% of the races).
- The failures always consistently fail with the same results.  Ie: If I run a test with mvn test, I'll always get one result.  If I run the same test with IntelliJ, I'll always get the same different result.

Maybe the result slightly changes if JVM decides to optimize a hot code path in case the JVM is reused. You might want to check how maven and your intellij run configuration are configured in terms of JVM forking.

-- J.

Tim Macpherson

unread,
Apr 28, 2025, 1:31:29 PMApr 28
to google-we...@googlegroups.com
Looks like a good game, I didn't see an option for 3d rendering, maybe still not practical in a browser


--
You received this message because you are subscribed to the Google Groups "GWT Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-web-tool...@googlegroups.com.
To view this discussion visit

Craig Mitchell

unread,
Apr 28, 2025, 8:17:20 PMApr 28
to GWT Users
> Amazing game! Congratulations
Thanks!  Just a fun side project that I'm probably getting too carried away with.

> Maybe the result slightly changes if JVM decides to optimize a hot code path in case the JVM is reused. You might want to check how maven and your intellij run configuration are configured in terms of JVM forking.

IntelliJ seems to add the options:
-Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8

So I added those to MAVEN_OPTS, but it didn't seem to make any difference.  I also tried adding @NotThreadSafe to my test suite, again no difference.

> Looks like a good game, I didn't see an option for 3d rendering, maybe still not practical in a browser

Thanks! Yes, 3D is very easy in the browser.  My game actually uses WebGL, so it's using the 3D card, just rendering a 2D scene.  Only reason I didn't do 3D is because my 3D modelling skills are non existent.  Plus I wanted to keep the game lightweight and dynamic, so every track has different cars and scenery.  2D is simple sprites, 3D is meshes and lots of textures.

Jens

unread,
Apr 29, 2025, 6:34:25 AMApr 29
to GWT Users
> Maybe the result slightly changes if JVM decides to optimize a hot code path in case the JVM is reused. You might want to check how maven and your intellij run configuration are configured in terms of JVM forking.

IntelliJ seems to add the options:
-Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8

So I added those to MAVEN_OPTS, but it didn't seem to make any difference.  I also tried adding @NotThreadSafe to my test suite, again no difference.

I meant that IntelliJ run configuration for JUnit has an option forkmode and I am pretty sure the maven plugin also has an option for JVM forking. For example you can configure that every test method should run in a forked VM in IntelliJ. By default it doesn't do that. 

-- J.

Craig Mitchell

unread,
Apr 30, 2025, 1:39:57 AMApr 30
to GWT Users
> I meant that IntelliJ run configuration for JUnit has an option forkmode and I am pretty sure the maven plugin also has an option for JVM forking. For example you can configure that every test method should run in a forked VM in IntelliJ. By default it doesn't do that. 

Ah, cool, thanks.  I see that option now.

download.png

With it on, I get different results again.  Different to maven, different to no fork, and different to JavaScript.

And if I run it through Maven with "mvn test -DforkCount=1 -DreuseForks=false", I get results that are completely different again.

They are still consistent though.  Ie: Running it multiple times, always gets the same results.

Craig Mitchell

unread,
May 12, 2025, 9:45:15 AMMay 12
to GWT Users
The results from GWTTestCase match JavaScript!

It's rather slow though.  Running the same test in these envs:
  • Java: 184ms.
  • JavaScript (Chrome): 3.5 seconds.
  • GWTTestCase: 5.8 minutes.

Jens

unread,
May 12, 2025, 11:50:08 AMMay 12
to GWT Users
It is really weird that your results do not match. Prior Java 17 the "strictfp" keyword could be used to force the JVM to not cheat a little. But since Java 17 it is the default and the keyword does nothing anymore (https://openjdk.org/jeps/306). So the results should be portable now.

In case you are using DoubleSummaryStatistics in your code, GWT emulation uses Kahan summation to compensate for rounding errors but I think Java uses the same algorithm. However JavaDoc of DoubleSummaryStatistics.getSum() leaves room to have different implementations and error compensations. It explicitly says that the output may vary for the same input.

-- J.

Colin Alworth

unread,
May 12, 2025, 3:13:31 PMMay 12
to GWT Users
Any chance you're using any JNI libraries in your JVM implementation? I recently became aware that gcc-compiled libraries with -ffast-math and friends can result in subnormal values doing unexpected things, causing inconsistent/incorrect results for some well defined operations. Unfortunately that flag can cause gcc to emit x86 assembly that makes global changes to how a process interacts with these numbers.


It appears that Java 22+ has mitigation for this, by testing in at least some cases if these features have been used and then attempting to restore the expected state for Java:

My read is that this is technically incomplete, since it only happens at loadLibrary() time, and technically any call into JNI could re-set these flags.

Craig Mitchell

unread,
May 12, 2025, 8:12:56 PMMay 12
to GWT Users
Thanks all.  I'm not using DoubleSummaryStatistics or any JNI.

One thing I noticed when I was logging my calcs, trying to compare the differences between Java and JavaScript, I would sometimes get -0.0 for a double.

Turns out there is a difference between 0.0 and -0.0, but (I think) only when using Double, not double (which I have in a few places).  Ie:

In Java:
Double a = 0.0;
Double b = -0.0;

if (a == b) System.out.println("yes");
else System.out.println("no");

Prints "no".

While in GWT:
Double a = 0.0;
Double b = -0.0;

if (a == b) GWT.log("yes");
else GWT.log("no");

Prints "yes".

I'll now remove all "Double"s from my code and see if that fixes it.  Fingers crossed!

Craig Mitchell

unread,
May 12, 2025, 10:13:00 PMMay 12
to GWT Users
Removed the Doubles.  It didn't solve it.

Bit of a long shot in hindsight, as getting different results from Java based on if I run the test via "mvn test", or via IntelliJ, is still the red flag.

Craig Mitchell

unread,
May 13, 2025, 12:00:44 AMMay 13
to GWT Users
I tried moving my tests out of JUnit, and into my main application (I can trigger them from an admin tool).
  • Running in JavaScript in the browser.  Consistent and correct results.
  • Running in Java Springboot on Google App Engine with an Undertow webserver.  Inconsistent and incorrect results.  Ie: I run the test once and get one result, run it again and it gives me a different result.
  • Running locally on my Windows PC (instead of Google App Engine).  Same inconsistent and incorrect results.
I also tried switching to use Tomcat instead of Undertow.  No difference.

The head scratching continues ...

Colin Alworth

unread,
May 13, 2025, 10:23:55 AMMay 13
to GWT Users
Negative zero is definitely a thing in Java for primitive doubles - use Double.compare(double,double) to check for negative zeroes. In jshell:
jshell> Double.compare(-0.0, 0.0)
$1 ==> -1

jshell> Double.compare(0.0, 0.0)
$2 ==> 0

jshell> Double.compare(0.0, -0.0)
$3 ==> 1

With that said, I wasn't aware that boxed doubles/floats in GWT don't quite behave as they do in the JVM, for zeroes and NaNs when boxed or in arrays:

This almost certainly isn't related to your issue though, which as you've noted, is JVM vs JVM.

Are you able to instrument the simulation at some kind of checkpoint (ideally per-calculation, but something coarser should help too), and then record one JVM's results as it runs, then have the other check its work to find the first point where it differs? That should help you work backwards to the actual point of divergence, ideally to understand which operation introduced the divergence, then get to why there is such a difference?

Jens

unread,
May 13, 2025, 3:26:28 PMMay 13
to GWT Users
The head scratching continues ...
 
As a last resort: Add -XX:+PrintFlagsFinal to your JVM parameters in IntelliJ and in Maven. It shows every flag the JVM has and its final value used by the JVM. Save both outputs to a file and make a diff to see any differences. Maybe some flags with regard to optimization are different and may affect double calculations.

-- J.

Craig Mitchell

unread,
May 13, 2025, 9:22:42 PMMay 13
to GWT Users
> Are you able to instrument the simulation at some kind of checkpoint

Good call Colin, thanks.  Managed to create a simple(ish) simulation that fails, and it tracks the game frame and cars position.  There is a clear reproducable point where the JVM starts to differ from JavaScript.

Screenshot 2025-05-14 111708.png

Still diving into the code to make sense of why, but progress!  Thanks again.

Craig Mitchell

unread,
May 14, 2025, 7:34:08 AMMay 14
to GWT Users
I found the problem, and it's my fault (and I'm a little embarrassed by it).

I had used a HashSet when the order was important.  GWT (JavaScript) maintains the order regardless.  Java does not.  Java will rearrange the order depending on how you run it, and any numerous other reasons.

When I switched it to be a LinkedHashSet, and all my tests started working.

Thank you everyone for all the help, and really sorry it just turned out to be my fault.
Reply all
Reply to author
Forward
0 new messages