IntStream 导致数组元素被错误地设置为 0(JVM Bug,Java 11)

2022-09-02 01:38:16

在下面的类中,该方法似乎返回相同:Ptestfalse

import java.util.function.IntPredicate;
import java.util.stream.IntStream;

public class P implements IntPredicate {
    private final static int SIZE = 33;

    @Override
    public boolean test(int seed) {
        int[] state = new int[SIZE];
        state[0] = seed;
        for (int i = 1; i < SIZE; i++) {
            state[i] = state[i - 1];
        }
        return seed != state[SIZE - 1];
    }

    public static void main(String[] args) {
        long count = IntStream.range(0, 0x0010_0000).filter(new P()).count();
        System.out.println(count);
    }
}

但是,将类与 组合在一起,该方法可以(错误地)返回 。上述方法中的代码会产生一些正整数,例如 。每次执行后,结果都会更改。PIntStreamtesttruemain716208

发生此意外行为的原因是,在执行过程中可以将数组设置为零。如果测试代码,例如intstate[]

if (seed == 0xf_fff0){
    System.out.println(Arrays.toString(state));
} 

插入到方法的尾部,然后程序将输出类似。test[1048560, 1048560, 1048560, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

问题:为什么可以将 int 数组设置为零?state[]

我已经知道如何避免这种行为:只需替换为.int[]ArrayList

我在以下方面进行了检查:

  • Windows 10 + 和 debian 10+ 与 OpenJDK 运行时环境 (构建 15.0.1+9-18) OpenJDK 64 位服务器虚拟机 (构建 15.0.1+9-18, 混合模式, 共享)
  • debian 9 + OpenJDK Runtime Environment AdoptOpenJDK (build 13.0.1+9) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 13.0.1+9, mixed mode, sharing)

答案 1

可以用一个更简单的例子重现这个问题,即:

class Main {
    private final static int SIZE = 33;

    public static boolean test2(int seed) {
        int[] state = new int[SIZE];
        state[0] = seed;
        for (int i = 1; i < SIZE; i++) {
            state[i] = state[i - 1];
        }
        return seed != state[SIZE - 1];
    }

    public static void main(String[] args) {
        long count = IntStream.range(0, 0x0010_0000).filter(Main::test2).count();
        System.out.println(count);

    }
}

该问题是由允许循环( )的矢量化 (SIMD) 的优化标志引起的。可能是由于对具有相交范围( )的同一数组应用矢量化而产生的。如果 (对于 的某些元素),则可能会重现类似的问题,优化循环:JVM-XX:+AllowVectorizeOnDemandstate[i] = state[i - 1];JVMIntStream.range(0, 0x0010_0000)

   for (int i = 1; i < SIZE; i++)
       state[i] = state[i - 1];

到:

    System.arraycopy(state, 0, state, 1, SIZE - 1);

例如:

class Main {
    private final static int SIZE = 33;

    public static boolean test2(int seed) {
        int[] state = new int[SIZE];
        state[0] = seed;
        System.arraycopy(state, 0, state, 1, SIZE - 1);
        if(seed == 100)
           System.out.println(Arrays.toString(state));
        return seed != state[SIZE - 1];
    }

    public static void main(String[] args) {
        long count = IntStream.range(0, 0x0010_0000).filter(Main::test2).count();
        System.out.println(count);
    }
}

输出:

[100, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

新更新: 01/01/2021

我已向参与该标志的实现/集成的开发人员之一发送电子邮件,收到了以下回复:-XX:+AllowVectorizeOnDemandand

众所周知,AllowVectorizeOnDemand代码的一部分被破坏了。

有修复(它排除了执行不正确的矢量化的损坏代码),它被向后移植到jdk 11.0.11中:

https://hg.openjdk.java.net/jdk-updates/jdk11u-dev/rev/69dbdd271e04

如果可以,请尝试从 https://hg.openjdk.java.net/jdk-updates/jdk11u-dev/ 构建和测试最新的 OpenJDK11u

从第一个链接中,可以阅读以下内容:

@bug 8251994 @summary Streams$RangeIntSpliterator::forEachRemaining @requires vm.compiler2.enabled & vm.compMode 的测试矢量化 != “Xint”

@run main compiler.vectorization.TestForEachRem test1 @run main compiler.vectorization.TestForEachRem test2 @run main compiler.vectorization.TestForEachRem test3 @run main compiler.vectorization.TestForEachRem test4

从JIRA关于该错误的故事的评论中,人们可以读到:

我找到了问题的原因。为了提高对循环进行矢量化的机会,superword 尝试通过将其内存输入替换为相应的(相同的内存片)循环的内存 Phi 来将负载提升到循环的开头:http://hg.openjdk.java.net/jdk/jdk/file/8f73aeccb27c/src/hotspot/share/opto/superword.cpp#l471

最初,加载按同一内存片上的相应存储进行排序。但是当他们被吊起时,他们失去了这种命令 - 没有什么可以执行命令。在 test6 案例中,仅当矢量大小为 32 字节 (avx2) 但以 16 (avx=0 或 avx1) 或 64 (avx512) 字节向量无序时,才会在提升后保留排序(幸运的是?)。(...)

我有简单的修复(使用原始加载排序索引),但是查看导致问题的代码,我发现它是虚假的/不完整的 - 它无助于JDK-8076284更改列出的案例:

https://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2015-April/017645.html

使用展开和克隆信息进行矢量化是一个有趣的想法,但正如我所看到的,它并不完整。即使pack_parallel()方法能够创建包,它们也会被filter_packs()方法删除。此外,上述情况在没有提升负载的情况下进行了矢量化,并pack_parallel - 我验证了它。该代码现在毫无用处,我将把它放在标志下不运行它。它需要更多的工作才能有用。我不愿意删除代码,因为将来我们可能会有时间投入其中。

这也许可以解释为什么当我比较带有和不带标志的版本汇编时,我注意到带有标志的版本用于以下代码:-XX:+AllowVectorizeOnDemand

   for (int i = 1; i < SIZE; i++)
       state[i] = state[i - 1];

(我在调用的方法上提取,以便于在程序集中查找它),具有:hotstop

00000001162bacf5: mov    %r8d,0x10(%rsi,%r10,4)
0x00000001162bacfa: mov    %r8d,0x14(%rsi,%r10,4)
0x00000001162bacff: mov    %r8d,0x18(%rsi,%r10,4)
0x00000001162bad04: mov    %r8d,0x1c(%rsi,%r10,4)
0x00000001162bad09: mov    %r8d,0x20(%rsi,%r10,4)
0x00000001162bad0e: mov    %r8d,0x24(%rsi,%r10,4)
0x00000001162bad13: mov    %r8d,0x28(%rsi,%r10,4)
0x00000001162bad18: mov    %r8d,0x2c(%rsi,%r10,4)  ;*iastore {reexecute=0 rethrow=0 return_oop=0}
                                             ; - AAAAAA.Main::hotstop@15 (line 21)

在我看来,这就像一个循环 ,从那边来看,该方法只出现在带有标志的版本的集合中。unrollingjava.util.stream.Streams$RangeIntSpliterator::forEachRemaining


答案 2