JDK-7196857 : Int array initializes with nonzero elements.
  • Type: Bug
  • Status: Resolved
  • Resolution: Cannot Reproduce
  • Component: hotspot
  • Sub-Component: compiler
  • Priority: P2
  • Affected Version: 7
  • OS: generic
  • CPU: generic
  • Submit Date: 2012-09-07
  • Updated Date: 2014-10-21
  • Resolved Date: 2014-10-21
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availabitlity Release.

To download the current JDK release, click here.
java version: all versions from 1.7.0_04 up to 1.7.0_10
Java HotSpot(TM) 64-Bit Server VM

All amd64  Linux, Windows, MacOS distributions.  Not tested on x86 OSes.

Primitive integer array has nonzero elements just after allocation. This situation is in the contradiction with JLS.  Such a behavior occurs after jvm performs the compilation of the corresponding place of the code and only if corresponding code contains Arrays.fill(..) statement somewhere after array initialization (but not just after initialization). For more details see simple code example below.



Just run the code listed below.

Expected: Build success
Actual: RuntimeException
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
public class JvmBug {
public static void main(String[] args) {
        int[] a;
        int n = 0;
        for (int i = 0; i < 100000000; ++i) {
            a = new int[10];
            for (int f : a)
                if (f != 0)
                    throw new RuntimeException("Array just after allocation: "+  Arrays.toString(a));
            Arrays.fill(a, 0);
            for (int j = 0; j < a.length; ++j)
                a[j] = (n - j)*i;
            for (int f : a)
                n += f;
---------- END SOURCE ----------

Add Arrays.fill(a,0) just after array initialization.

This bug disappeared because of other changes that disabled an optimization; arguably, it is a duplicate. Someone might want to file an RFE for the missing performance, but it-would-be-nice if any such RFE also included enough benchmarking to allow us to understand (or not) the potential gains.

@John: Ah, I didn't know about that one. Thanks. But the important point is to keep the allocation and the initialization together so that the runtime compiler doesn't have to work too hard to figure out that it doesn't need to zero the array before pointing parallelSetAll at the individual elements. That is, I want a method in Arrays that combines the allocation and initialization so the compiler can know the semantics.

@Peter: java.util.Arrays.parallelSetAll gives everything except the allocation part.

It would be nice if I could supply code to initialize arrays. The problem, obviously, is not to allow the reference to the array to escape before the array is completely initialized, or to read the elements of the array before they are initialized. Based on some discussions with David Leibs, the following might be workable. Define a class, somewhere in the JRE, that is the default initializer. E.g., for initializing an array of doubles class Initializer { // The default initializer returns 0.0 for every element. double apply(int i) { return 0.0; } } (and I'd need one of these for all the primitive types). Then add a library method, e.g., to class Arrays public static double[] initializerDouble(int len, Initializer init) { final double[] result = new double[len]; for (int i = 0; i < len; i += 1) { result[i] = init.apply(i); } return result; } whose jobs is to allocate the array, but not expose it or its elements, until all the elements have been initialized. Then I can use that as in double[] iArray = initializerDouble(7, new Initializer() { double apply(int i) { return (double) i; } }); to get an array whose elements are initialized with their offset in the array. The point is that Initializer.apply(int) does *not* get a reference to the array, so it can't peek into it, or leak it, and if the initialization code exits abruptly, the reference to the array is dropped on the floor and thus not exposed. This does not work for Object[], because the garbage collector needs to see nulls or valid references in all the elements. Since the Initializer instance is a closure, it could do arbitrary computations, using (final) variables from its scope, etc. If I have to reify a big array to be the result of a computation, I can construct an Initializer that does the computation, rather than initialize all the elements to 0.0 and then reassign them all with the value of the computation. One trip through the cache, etc. Then I would want the runtime compiler to identify calls to InitializerDouble() and generate good code for it. In the default case generate the code we have now to set all the elements to 0.0, or in the easy cases inlining the call to Initializer.apply() and unrolling the loop, inserting non-temporal prefetches, wide registers, etc. Identifying the call seems easier than the pattern-matching in PhaseIdealLoop::match_fill_loop(). In my dreams it would parallelize the initialization of the array, since otherwise initializing a big array is a serial piece of what might otherwise be an embarrassingly parallel bunch of math on the individual elements, and Amdahl will come and bite my ankle.

Peter B. Kessler wrote: In src/share/vm/opto/loopTransform.cpp, the comment 2404 // Process the loop looking for stores. If there are multiple 2405 // stores or extra control flow give at this point. "give at this point" should say "give up at this point". I would move that sentence down to where the code gives up, e.g., 2410 if (n->is_Store()) { 2411 if (store != NULL) { + //If there are multiple stores give up at this point. 2412 msg = "multiple stores"; 2413 break; 2414 } .... 2428 } else if (n->is_If() && n != head->loopexit()) { + // If there is extra control flow give up at this point. 2429 msg = "extra control flow"; 2430 msg_node = n; 2431 } There are several other reasons that code gives up (oop assignments, negative strides, etc.) that deserve equal documentation, which might mean not calling out these two specially.

(Lots of run/debug options here for future reference, and in case anyone wants to check my work) Something has changed in the processing upstream of the bug, and (in the debugger) it no longer comes near the code that might commit this error in the bug itself. Compiling the JDK library for a normal run (i.e., -Xcomp) the path is taken, but not for the bug itself (here, the test was reorganized slightly into main and test, within a package, because failure to trigger the bug caused doubts about corner cases in CompileOnly behavior): -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:CompileOnly=pack/JvmBug.main,pack.JvmBug.test pack.JvmBug) Breakpoints that should have triggered but did not: LibraryCallKit::tightly_coupled_allocation(Node*, RegionNode*) at library_call.cpp:4951 LibraryCallKit::generate_arraycopy(TypePtr const*, BasicType, Node*, Node*, Node*, Node*, Node*, bool, bool, RegionNode*) (gdb) break LibraryCallKit::inline_arraycopy Breakpoint 4 at 0x102192e29: file library_call.cpp, line 4410. To verify compilation at all, I set this breakpoint, which was triggered: C2Compiler::compile_method(ciEnv*, ciMethod*, int) at c2compiler.cpp:122 Stack traces compiling the library confirmed that the breakpoints listed above should all be triggered before a call to tightly_coupled_allocation.

Oddly enough, did not reproduce for me. x86_64, Mac, Java(TM) SE Runtime Environment (build 1.7.0_21-b12) Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode) or with java version "1.8.0-internal-fastdebug" Java(TM) SE Runtime Environment (build 1.8.0-internal-fastdebug-dr2chase_2013_06_10_15_00-b00) Java HotSpot(TM) 64-Bit Server VM (build 25.0-b37-fastdebug, mixed mode) was this fixed accidentally? I will continue to investigate.

The problem is caused by: when optimizing Arrays.fill() calls, C2 checks if that call is tightly coupled with an array allocation and omits the implicit zeroing if it is; but it's missing necessary checks of the use of the implicit zero'd values, so it may mistakenly omit the zeroing even when there are uses of the array's contents in between the allocation site and the Arrays.fill() call. The fix would have to enforce the check so that it respects the uses of implicit zero'd values.

EVALUATION The bug reproduces with the fill or with a zeroing for loop, but not without. So compiler correctly matches a complete array fill, but the check for uses between the allocation and the fill is faulty.