为什么边界检查没有被消除?答案摘要新闻

我写了一个简单的基准测试,以便找出当数组通过按位和计算时是否可以消除边界检查。这基本上就是几乎所有哈希表的作用:它们计算

h & (table.length - 1)

作为索引进入 ,其中 是 或 派生值。结果显示边界检查不会被消除。tablehhashCode

我的基准测试的想法非常简单:计算两个值和 ,其中两者都保证是有效的数组索引。ij

  • 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];
}

其他实验用途

    result ^= table[i] + j;

相反。时间上的差异可能是15%(在我尝试过的不同变体中非常一致)。我的问题:

  • 除了边界检查消除之外,还有其他可能的原因吗?
  • 有没有一些复杂的原因,我不明白为什么没有绑定检查消除?j

答案摘要

MarkoTopolnik的回答表明,这一切都更加复杂,消除边界检查并不能保证是胜利,特别是在他的计算机上,“正常”代码比“屏蔽”慢。我想这是因为它允许一些额外的优化,在这种情况下实际上是有害的(考虑到当前CPU的复杂性,编译器甚至很难确定)。

leventov的答案清楚地表明,数组边界检查是在“屏蔽”中完成的,并且它的消除使代码与“正常”一样快。

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

建议的优化

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

  • 对于每个数组访问,检查索引是否通过按位和计算。
  • 如果是这样,请检查是否将任一操作数计算为长度减去 1。
  • 如果是这样,请将边界检查替换为零长度检查。
  • 让现有的优化来处理它。

这样的优化应该非常简单和便宜,因为它只查看SSA图中的父节点。与许多复杂的优化不同,它永远不会是有害的,因为它只能用稍微简单的检查替换一个检查;所以没有问题,即使它不能被移出循环。

我会把它发布到热点开发邮件列表。

新闻

约翰·罗斯(John Rose)提交了RFE,已经有了一个“快速而肮脏”的补丁


答案 1

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

我的猜想:

边界将数字作为循环退出点比作为引入开销的附加代码更强烈地检查数字

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

  • 循环是展开的(在所有情况下都是如此);
  • 此外,首先对所有展开的步骤从阵列级进行提取,然后对所有步骤执行累加器的异形。

如果循环可以在任何步骤中中断,则此暂存将导致对从未实际执行的循环步骤执行的工作。

请考虑对代码的轻微修改:

@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;
  }
}

只有一个区别:我添加了支票

if (entry == 0) break;

为循环提供在任何步骤中过早退出的方法。(我还引入了一个保护装置,以确保没有数组条目实际上为 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

正如一般预期的那样,“正常指数”变体要快得多。

但是,让我们删除附加检查

// 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

“屏蔽索引”的响应是可预测的(减少了开销),但“正常索引”突然变得更糟。这显然是由于额外的优化步骤与我的特定CPU模型之间的不匹配。

我的观点:

如此详细级别的性能模型非常不稳定,并且正如我在CPU上看到的那样,甚至不稳定。


答案 2
  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;
    }
}

结果:

Benchmark                                Mean   Mean error    Units
BCElimination.maskedIndex               1,235        0,004    ns/op
BCElimination.maskedIndexUnsafe         1,092        0,007    ns/op
BCElimination.normalIndex               1,071        0,008    ns/op


2.第二个问题是针对热点开发邮件列表,而不是StackOverflow,恕我直言。


推荐