Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/asp.net-mvc/14.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Java 为什么边界检查没有';你不会被淘汰吗?_Java_Optimization_Microbenchmark_Bounds Check Elimination - Fatal编程技术网

Java 为什么边界检查没有';你不会被淘汰吗?

Java 为什么边界检查没有';你不会被淘汰吗?,java,optimization,microbenchmark,bounds-check-elimination,Java,Optimization,Microbenchmark,Bounds Check Elimination,我编写了一个简单的示例,以了解当通过按位and计算数组时,是否可以消除边界检查。这就是几乎所有哈希表的基本功能:它们计算 h & (table.length - 1) 作为表的索引,其中h是hashCode或派生值。这表明边界检查并没有被消除 我的基准测试思想非常简单:计算两个值i和j,这两个值都保证是有效的数组索引 i是循环计数器。当它被用作数组索引时,边界检查被消除 j计算为x&(table.length-1),其中x是在每次迭代中更改的某个值。当它被用作数组索引时,边界检查不会

我编写了一个简单的示例,以了解当通过按位and计算数组时,是否可以消除边界检查。这就是几乎所有哈希表的基本功能:它们计算

h & (table.length - 1)
作为
表的索引,其中
h
hashCode
或派生值。这表明边界检查并没有被消除

我的基准测试思想非常简单:计算两个值
i
j
,这两个值都保证是有效的数组索引

  • i
    是循环计数器。当它被用作数组索引时,边界检查被消除
  • j
    计算为
    x&(table.length-1)
    ,其中
    x
    是在每次迭代中更改的某个值。当它被用作数组索引时,边界检查不会被消除
有关部分如下:

for (int i=0; i<=table.length-1; ++i) {
    x += result;
    final int j = x & (table.length-1);
    result ^= i + table[j];
}
相反。时间上的差异可能是15%(我尝试过的不同变体之间的差异非常一致)。我的问题是:

  • 除了取消绑定支票之外,还有其他可能的原因吗
  • 有什么复杂的原因我不明白为什么
    j
    没有绑定检查消除
答案摘要 MarkoTopolnik的回答表明,这一切都更加复杂,取消边界检查并不能保证是一场胜利,特别是在他的计算机上,“正常”代码比“屏蔽”代码慢。我猜这是因为它允许一些额外的优化,这在这种情况下实际上是有害的(考虑到当前CPU的复杂性,编译器甚至很难确定)

leventov的回答清楚地表明,数组边界检查是在“蒙面”中完成的,它的消除使代码与“正常”一样快

Donal Fellows指出了这样一个事实,屏蔽对于零长度的表不起作用,因为
x&(0-1)
等于
x
。因此,编译器能做的最好的事情就是用零长度检查替换绑定检查。但这仍然是值得的,因为零长度检查可以很容易地移出循环

建议的优化 由于等价性
a[x&(a.length-1)]
当且仅当
a.length==0
时抛出,编译器可以执行以下操作:

  • 对于每个数组访问,检查是否已通过按位and计算索引
  • 如果是,请检查两个操作数中是否有一个计算为长度减1
  • 如果是,则用零长度检查替换边界检查
  • 让现有的优化来解决这个问题
这样的优化应该非常简单和便宜,因为它只查看图中的父节点。与许多复杂的优化不同,它永远不会有害,因为它只会用一个稍微简单的检查来代替一个检查;所以没有问题,即使它不能从循环中移出

我会将此发布到热点开发人员邮件列表

新闻 约翰·罗斯提交了一份申请,已经有一份“又快又脏”的申请了

  • 不,这显然是由于没有足够的智能边界检查消除造成的
  • 我扩展了Marko Topolnik的基准:

    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    @OperationsPerInvocation(BCElimination.N)
    @Warmup(iterations = 5, time = 1)
    @Measurement(iterations = 10, time = 1)
    @State(Scope.Thread)
    @Threads(1)
    @Fork(2)
    public class BCElimination {
        public static final int N = 1024;
        private static final Unsafe U;
        private static final long INT_BASE;
        private static final long INT_SCALE;
        static {
            try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                U = (Unsafe) f.get(null);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
    
            INT_BASE = U.arrayBaseOffset(int[].class);
            INT_SCALE = U.arrayIndexScale(int[].class);
        }
    
        private final int[] table = new int[BCElimination.N];
    
        @Setup public void setUp() {
            final Random random = new Random();
            for (int i=0; i<table.length; ++i) table[i] = random.nextInt();
        }
    
        @GenerateMicroBenchmark public int normalIndex() {
            int result = 0;
            final int[] table = this.table;
            int x = 0;
            for (int i=0; i<=table.length-1; ++i) {
                x += i;
                final int j = x & (table.length-1);
                result ^= table[i] + j;
            }
            return result;
        }
    
        @GenerateMicroBenchmark public int maskedIndex() {
            int result = 0;
            final int[] table = this.table;
            int x = 0;
            for (int i=0; i<=table.length-1; ++i) {
                x += i;
                final int j = x & (table.length-1);
                result ^= i + table[j];
            }
            return result;
        }
    
        @GenerateMicroBenchmark public int maskedIndexUnsafe() {
            int result = 0;
            final int[] table = this.table;
            long x = 0;
            for (int i=0; i<=table.length-1; ++i) {
                x += i * INT_SCALE;
                final long j = x & ((table.length-1) * INT_SCALE);
                result ^= i + U.getInt(table, INT_BASE + j);
            }
            return result;
        }
    }
    


    2.第二个问题是热点开发人员邮件列表,而不是StackOverflow,IMHO。

    为了安全地消除边界检查,有必要证明

    h & (table.length - 1)
    
    保证在
    中生成有效索引。如果
    table.length
    为零,则不会出现这种情况(因为您将得到
    和-1
    ,这是一个有效的noop)。如果<代码>表,长度< <代码>不是2的幂(您将丢失信息;考虑“代码>表。长度< /代码>为17”的情况)。
    热点编译器如何知道这些坏条件不是真的?它必须比程序员更为保守,因为程序员可以更多地了解系统的高级约束(例如,数组从来都不是空的,并且总是作为一个二次幂的元素数)。

    首先,两个测试之间的主要区别肯定是边界检查消除;然而,这影响机器代码的方式与天真的期望相去甚远

    我的猜测是: 边界检查作为循环出口点的作用比作为额外代码的作用更大,这会引入开销

    循环退出点阻止了我从发出的机器代码中剔除的以下优化:

    • 循环展开(在所有情况下都是如此)
    • 此外,首先对所有展开的步骤执行从数组阶段的获取,然后对所有步骤执行xoring到累加器
    如果循环可以在任何步骤中中断,则此分段将导致为从未实际执行的循环步骤执行工作

    考虑一下对代码的这种轻微修改:

    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    @OperationsPerInvocation(Measure.N)
    @Warmup(iterations = 3, time = 1)
    @Measurement(iterations = 5, time = 1)
    @State(Scope.Thread)
    @Threads(1)
    @Fork(1)
     public class Measure {
      public static final int N = 1024;
    
      private final int[] table = new int[N];
      @Setup public void setUp() {
        final Random random = new Random();
        for (int i = 0; i < table.length; ++i) {
          final int x = random.nextInt();
          table[i] = x == 0? 1 : x;
        }
      }
      @GenerateMicroBenchmark public int normalIndex() {
        int result = 0;
        final int[] table = this.table;
        int x = 0;
        for (int i = 0; i <= table.length - 1; ++i) {
          x += i;
          final int j = x & (table.length - 1);
          final int entry = table[i];
          result ^= entry + j;
          if (entry == 0) break;
        }
        return result;
      }
      @GenerateMicroBenchmark public int maskedIndex() {
        int result = 0;
        final int[] table = this.table;
        int x = 0;
        for (int i = 0; i <= table.length - 1; ++i) {
          x += i;
          final int j = x & (table.length - 1);
          final int entry = table[j];
          result ^= i + entry;
          if (entry == 0) break;
        }
        return result;
      }
    }
    
    为循环提供在任何步骤上提前退出的方法。(我还引入了一个保护,以确保没有数组条目实际上是0。)

    在我的机器上,结果如下:

    Benchmark                   Mode   Samples         Mean   Mean error    Units
    o.s.Measure.maskedIndex     avgt         5        1.378        0.229    ns/op
    o.s.Measure.normalIndex     avgt         5        0.924        0.092    ns/op
    
    正如人们普遍预期的那样,“正常指数”变量的速度要快得多

    但是,让我们取消附加检查:

    现在我的结果是:

    Benchmark                   Mode   Samples         Mean   Mean error    Units
    o.s.Measure.maskedIndex     avgt         5        1.130        0.065    ns/op
    o.s.Measure.normalIndex     avgt         5        1.229        0.053    ns/op
    
    “隐藏指数”的反应是可预测的(减少了开销),但“正常指数”突然变得更糟。这显然是由于额外的优化步骤和我的特定CPU型号之间的不匹配

    我的观点是:
    这种详细级别的性能模型非常不稳定,正如我在CPU上看到的,甚至不稳定。

    我看到了一个可能的原因:
    表[I]
    导致顺序访问模式,而
    表[j]
    则更不规则。仅一次或两次缓存未命中就足以解释15%的差异。顺便说一句,除了过滤掉所有不在缓存中的内容外,还可以使用选项
    -XX:CompileCommand=print,*Benchmark.time*
    if (entry == 0) break;
    
    Benchmark                   Mode   Samples         Mean   Mean error    Units
    o.s.Measure.maskedIndex     avgt         5        1.378        0.229    ns/op
    o.s.Measure.normalIndex     avgt         5        0.924        0.092    ns/op
    
    // if (entry == 0) break;
    
    Benchmark                   Mode   Samples         Mean   Mean error    Units
    o.s.Measure.maskedIndex     avgt         5        1.130        0.065    ns/op
    o.s.Measure.normalIndex     avgt         5        1.229        0.053    ns/op