The output -1 becomes a slash in the loop
Surprisingly, the following code outputs:
/
-1
The code:
public class LoopOutPut {
public static void main(String[] args) {
LoopOutPut loopOutPut = new LoopOutPut();
for (int i = 0; i < 30000; i++) {
loopOutPut.test();
}
}
public void test() {
int i = 8;
while ((i -= 3) > 0) ;
String value = i + "";
if (!value.equals("-1")) {
System.out.println(value);
System.out.println(i);
}
}
}
I tried many times to determine how many times this would occur, but, unfortunately, it was ultimately uncertain, and I found that the output of -2 sometimes turned into a period. In addition, I also tried to remove the while loop and output -1 without any problems. Who can tell me why?
JDK version information:
HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1
This can be reliably reproduced (or not reproduced, depending on what you want) with openjdk version "1.8.0_222"
(used in my analysis), OpenJDK 12.0.1
(according to Oleksandr Pyrohov) and OpenJDK 13 (according to Carlos Heuberger).
I ran the code with -XX:+PrintCompilation
enough times to get both behaviours and here are the differences.
Buggy implementation (displays output):
--- Previous lines are identical in both
54 17 3 java.lang.AbstractStringBuilder::<init> (12 bytes)
54 23 3 LoopOutPut::test (57 bytes)
54 18 3 java.lang.String::<init> (82 bytes)
55 21 3 java.lang.AbstractStringBuilder::append (62 bytes)
55 26 4 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
55 20 3 java.lang.StringBuilder::<init> (7 bytes)
56 19 3 java.lang.StringBuilder::toString (17 bytes)
56 25 3 java.lang.Integer::getChars (131 bytes)
56 22 3 java.lang.StringBuilder::append (8 bytes)
56 27 4 java.lang.String::equals (81 bytes)
56 10 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) made not entrant
56 28 4 java.lang.AbstractStringBuilder::append (50 bytes)
56 29 4 java.lang.String::getChars (62 bytes)
56 24 3 java.lang.Integer::stringSize (21 bytes)
58 14 3 java.lang.String::getChars (62 bytes) made not entrant
58 33 4 LoopOutPut::test (57 bytes)
59 13 3 java.lang.AbstractStringBuilder::append (50 bytes) made not entrant
59 34 4 java.lang.Integer::getChars (131 bytes)
60 3 3 java.lang.String::equals (81 bytes) made not entrant
60 30 4 java.util.Arrays::copyOfRange (63 bytes)
61 25 3 java.lang.Integer::getChars (131 bytes) made not entrant
61 32 4 java.lang.String::<init> (82 bytes)
61 16 3 java.util.Arrays::copyOfRange (63 bytes) made not entrant
61 31 4 java.lang.AbstractStringBuilder::append (62 bytes)
61 23 3 LoopOutPut::test (57 bytes) made not entrant
61 33 4 LoopOutPut::test (57 bytes) made not entrant
62 35 3 LoopOutPut::test (57 bytes)
63 36 4 java.lang.StringBuilder::append (8 bytes)
63 18 3 java.lang.String::<init> (82 bytes) made not entrant
63 38 4 java.lang.StringBuilder::append (8 bytes)
64 21 3 java.lang.AbstractStringBuilder::append (62 bytes) made not entrant
Correct run (no display):
--- Previous lines identical in both
55 23 3 LoopOutPut::test (57 bytes)
55 17 3 java.lang.AbstractStringBuilder::<init> (12 bytes)
56 18 3 java.lang.String::<init> (82 bytes)
56 20 3 java.lang.StringBuilder::<init> (7 bytes)
56 21 3 java.lang.AbstractStringBuilder::append (62 bytes)
56 26 4 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
56 19 3 java.lang.StringBuilder::toString (17 bytes)
57 22 3 java.lang.StringBuilder::append (8 bytes)
57 24 3 java.lang.Integer::stringSize (21 bytes)
57 25 3 java.lang.Integer::getChars (131 bytes)
57 27 4 java.lang.String::equals (81 bytes)
57 28 4 java.lang.AbstractStringBuilder::append (50 bytes)
57 10 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) made not entrant
57 29 4 java.util.Arrays::copyOfRange (63 bytes)
60 16 3 java.util.Arrays::copyOfRange (63 bytes) made not entrant
60 13 3 java.lang.AbstractStringBuilder::append (50 bytes) made not entrant
60 33 4 LoopOutPut::test (57 bytes)
60 34 4 java.lang.Integer::getChars (131 bytes)
61 3 3 java.lang.String::equals (81 bytes) made not entrant
61 32 4 java.lang.String::<init> (82 bytes)
62 25 3 java.lang.Integer::getChars (131 bytes) made not entrant
62 30 4 java.lang.AbstractStringBuilder::append (62 bytes)
63 18 3 java.lang.String::<init> (82 bytes) made not entrant
63 31 4 java.lang.String::getChars (62 bytes)
We can notice one significant difference. With the correct execution we compile test()
twice. Once in the beginning, and once again afterwards (presumably because the JIT notices how hot the method is). In the buggy execution test()
is compiled (or decompiled) 5 times.
Additionally, running with -XX:-TieredCompilation
(which either interprets, or uses C2
) or with -Xbatch
(which forces the compilation to run in the main thread, instead of parallelly), the output is guaranteed and with 30000 iterations prints out a lot of stuff, so the C2
compiler seems to be the culprit. This is confirmed by running with -XX:TieredStopAtLevel=1
, which disables C2
and doesn't produce output (stopping at level 4 shows the bug again).
In the correct execution, the method is first compiled with Level 3 compilation, then afterwards with Level 4.
In the buggy execution, the previous compilations are discared (made non entrant
) and it's again compiled on Level 3 (which is C1
, see previous link).
So it definitely is a bug in C2
, although I'm not absolutely sure whether the fact that it's going back to Level 3 compilation affects it (and why is it going back to level 3, so many uncertainties still).
You can generate the assembly code with the following line to go even deeper into the rabbit hole (also see this to enable assembly printing).
java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm
At this point I'm starting to run out of skills, the buggy behaviour starts to exhibit when the previous compiled versions are discarded, but what little assembly skills I have are from the 90's, so I'll let someone smarter than me take it from here.
It's likely that there is already a bug report about this, since the code was presented to the OP by someone else, and as all code C2 isn't without bugs. I hope this analysis has been as informative to others as it has been to me.
As the venerable apangin pointed out in the comments, this is a recent bug. Much obliged to all the interested and helpful people :)
This is honestly pretty odd, as that code should technically never output because ...
int i = 8;
while ((i -= 3) > 0);
... should always result in i
being -1
(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). What is even weirder is that it never outputs in the debug mode of my IDE.
Interestingly, the moment I add a check before the conversion to a String
, then no issue...
public void test() {
int i = 8;
while ((i -= 3) > 0);
if(i != -1) { System.out.println("Not -1"); }
String value = String.valueOf(i);
if (!"-1".equalsIgnoreCase(value)) {
System.out.println(value);
System.out.println(i);
}
}
Just two points of good coding practice ...
- Rather use
String.valueOf()
- Some coding standards specify that String literals should be the target of
.equals()
, rather than argument, thus minimizing NullPointerExceptions.
The only way I got this not to occur was by using String.format()
public void test() {
int i = 8;
while ((i -= 3) > 0);
String value = String.format("%d", i);
if (!"-1".equalsIgnoreCase(value)) {
System.out.println(value);
System.out.println(i);
}
}
... essentially it just looks like Java needs a little bit of time to catch its breath :)
EDIT: This may be completely coincidental, but there does seem to be some correspondence between the value that is printing out and the ASCII Table.
-
i
=-1
, character displayed is/
(ASCII decimal value of 47) -
i
=-2
, character displayed is.
(ASCII decimal value of 46) -
i
=-3
, character displayed is-
(ASCII decimal value of 45) -
i
=-4
, character displayed is,
(ASCII decimal value of 44) -
i
=-5
, character displayed is+
(ASCII decimal value of 43) -
i
=-6
, character displayed is*
(ASCII decimal value of 42) -
i
=-7
, character displayed is)
(ASCII decimal value of 41) -
i
=-8
, character displayed is(
(ASCII decimal value of 40) -
i
=-9
, character displayed is'
(ASCII decimal value of 39)
What is really interesting is that the character at ASCII decimal 48 is the value 0
and 48 - 1 = 47 (character /
), etc...
Don't know why Java is giving such random output but the issue is in your concatenation that fails for larger values of i
inside the for
loop.
If you replace String value = i + "";
line with String value = String.valueOf(i) ;
your code works as expected.
Concatenation using +
to convert the int to string is native and might be buggy (Oddly we are founding it now probably) and causing such issue.
Note: I reduced the value of i inside for loop to 10000 and I didn't face issue with +
concatenation.
This issue must be reported to Java stakeholders & they can give their opinion on same.
Edit I updated the value of i in for loop to 3 million and saw a new set of errors as below:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
at java.lang.Integer.getChars(Integer.java:463)
at java.lang.Integer.toString(Integer.java:402)
at java.lang.String.valueOf(String.java:3099)
at solving.LoopOutPut.test(LoopOutPut.java:16)
at solving.LoopOutPut.main(LoopOutPut.java:8)
My Java version is 8.