On Thu, 17 Sep 2026 20:57:05 GMT, Boris Ulasevich <[email protected]> 
wrote:

>> A for-each loop over ArrayList.Itr carries two phases of one recurrence 
>> through the loop: cursor (after the increment) and lastRet (before it). Both 
>> are live at the same time, so the allocator puts a copying mov in the loop 
>> body - on every iteration, for a value the loop never reads. This can be 
>> dealt with at the Java level, see #32344, but this change is an attempt to 
>> improve it at the level of C2 compilation.
>> 
>> In a loop compiled by C2, lastRet is never written to memory: the iterator 
>> is scalarized and the field lives in a register. In a plain for-each nobody 
>> reads it (it is only used by set() and remove()) so its only consumers are 
>> the Phi at the loop exit and the safepoint's debug info.
>> 
>> C2 already knows how to get rid of a second index: 
>> PhaseIdealLoop::replace_parallel_iv looks for a variable that walks 
>> alongside the trip counter and rewrites its uses in terms of the counter.
>> It only recognizes PARALLEL shape:
>> 
>> 
>> PARALLEL: int a = 5;     for (int iv = 0; iv < limit; iv++) { use(a);    a 
>> += 3;    }   
>> PREVIOUS: int prev = -1; for (int iv = 0; iv < limit; iv++) { use(prev); 
>> prev = iv; }
>> 
>> 
>> The patch teaches the recognition step the second (PREVIOUS) shape: a phi in 
>> the loop head whose backedge value is the trip counter plus a constant. Its 
>> uses are then rewritten in terms of the trip counter, the phi becomes dead 
>> code, and the copy in the loop body is no longer generated.
>> 
>> With `-XX:LoopMaxUnroll=1` the resulting aarch64 assembly looks like the 
>> following:
>> 
>> @Benchmark
>> public void list_foreach(Blackhole bh) {
>>     for (Object o : list) {
>>         bh.consume(o);
>>     }
>> }
>> 
>> BEFORE                               AFTER 
>>   mov  w14, w11                        -
>>   add  x11, x10, w14, sxtw #2          add  x12, x10, w16, sxtw #2
>>   ldr  w11, [x11, #0xc]                ldr  w12, [x12, #0xc]
>>   lsl  x11, x11, #3                    add  w16, w16, #0x1
>>   add  w11, w14, #0x1                  lsl  x12, x12, #3
>>   cmp  w11, w12                        cmp  w16, w14
>>   b.lt #-0x18                          b.lt #-0x14
>> 
>> 
>> Removing one instruction gives a speedup of up to 40% with 
>> `-XX:LoopMaxUnroll=1`, while with default VM options the picture is mixed: a 
>> 11–15% gain on some CPUs and nothing measurable on others.
>> 
>> ---------
>> - [x] I confirm that I make this contribution in accordance with the 
>> [OpenJDK Interim AI Policy](https://openjdk.org/legal/ai).
>
> Boris Ulasevich has updated the pull request incrementally with two 
> additional commits since the last revision:
> 
>  - TestParallelIvInIntCountedLoop: assert no loop is left, and test the 
> peeled case
>  - generalize the entry value check: any two values differing by a constant

src/hotspot/share/opto/loopnode.cpp line 4567:

> 4565: 
> 4566: // If (n2 - n1) is a constant, return it in *offset.
> 4567: static bool is_constant_difference(Node* n2, Node* n1, jint* offset) {

It should be preferable to use a reference here unless you want `offset` to be 
nullable.

test/hotspot/jtreg/compiler/loopopts/parallel_iv/TestParallelIvInIntCountedLoop.java
 line 405:

> 403:     @Test
> 404:     @IR(failOn = { IRNode.LOOP, IRNode.COUNTED_LOOP })
> 405:     private static int testIntCountedLoopWithDoubledIv(int stop) {

Can you add a test case where `prev = i + c`? All these cases are `prev = i`.

-------------

PR Review Comment: https://git.openjdk.org/jdk/pull/32549#discussion_r4043047987
PR Review Comment: https://git.openjdk.org/jdk/pull/32549#discussion_r4043087930

Reply via email to