Почему JIT так плохо справляется с устранением связанных проверок?
Я тестирую возможности исключения HotSpot JIT для связанных проверок. Вот две версии одной и той же реализации heapsort, одна использует обычное индексирование массива, другая sun.misc.Unsafe
API, без обязательных проверок:
public class HeapSort {
// copied from http://en.wikibooks.org/wiki/Algorithm_Implementation/Sorting/Heapsort#C
static int heapSortSimple(int[] arr) {
int t;
int n = arr.length, parent = n / 2, index, childI;
while (true) {
if (parent > 0) {
t = arr[--parent]; // 1, i. e. first indexing
} else {
if (--n == 0) break;
t = arr[n]; // 2
arr[n] = arr[0]; // 3, 4
}
index = parent;
childI = (index << 1) + 1;
while (childI < n) {
int childV = arr[childI]; // 5
int right;
if (childI + 1 < n && (right = arr[childI + 1]) > childV) { // 6
childI++;
childV = right;
}
if (childV > t) {
arr[index] = childV; // 7
index = childI;
childI = (index << 1) + 1;
} else {
break;
}
}
arr[index] = t; // 8
}
return arr[arr.length - 1];
}
static int heapSortUnsafe(int[] arr) {
int t;
long n = arr.length * INT_SCALE, parent = (arr.length / 2) * INT_SCALE, index, childI;
while (true) {
if (parent > 0) {
t = U.getInt(arr, INT_BASE + (parent -= INT_SCALE));
} else {
if ((n -= INT_SCALE) == 0) break;
t = U.getInt(arr, INT_BASE + n);
U.putInt(arr, INT_BASE + n, U.getInt(arr, INT_BASE));
}
index = parent;
childI = (index << 1) + INT_SCALE;
while (childI < n) {
int childV = U.getInt(arr, INT_BASE + childI);
int right;
if (childI + INT_SCALE < n &&
(right = U.getInt(arr, INT_BASE + (childI + INT_SCALE))) > childV) {
childI += INT_SCALE;
childV = right;
}
if (childV > t) {
U.putInt(arr, INT_BASE + index, childV);
index = childI;
childI = (index << 1) + INT_SCALE;
} else {
break;
}
}
U.putInt(arr, INT_BASE + index, t);
}
return arr[arr.length - 1];
}
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@State(Scope.Thread)
@Threads(1)
@Fork(1)
public static class Benchmarks {
static final int N = 1024;
int[] a = new int[N];
@Setup(Level.Invocation)
public void fill() {
Random r = ThreadLocalRandom.current();
for (int i = 0; i < N; i++) {
a[i] = r.nextInt();
}
}
@GenerateMicroBenchmark
public static int simple(Benchmarks st) {
int[] arr = st.a;
return heapSortSimple(arr);
}
@GenerateMicroBenchmark
public static int unsafe(Benchmarks st) {
int[] arr = st.a;
return heapSortUnsafe(arr);
}
}
public static void main(String[] args) {
Benchmarks bs = new Benchmarks();
// verify simple sort
bs.fill();
int[] a1 = bs.a;
int[] a2 = a1.clone();
Arrays.sort(a2);
heapSortSimple(a1);
if (!Arrays.equals(a2, a1))
throw new AssertionError();
// let JIT to generate optimized assembly
for (int i = 0; i < 10000; i++) {
bs.fill();
heapSortSimple(bs.a);
}
// verify unsafe sort
bs.fill();
a1 = bs.a;
a2 = a1.clone();
Arrays.sort(a2);
heapSortUnsafe(a1);
if (!Arrays.equals(a2, a1))
throw new AssertionError();
for (int i = 0; i < 10000; i++) {
bs.fill();
heapSortUnsafe(bs.a);
}
}
static final Unsafe U;
static final long INT_BASE;
static final long INT_SCALE = 4;
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);
}
}
Небезопасная версия стабильно на 13% быстрее как для процессоров Intel SB, так и для процессоров AMD K10.
Я посмотрел в сгенерированную сборку:
- проверка нижней границы устраняется для всех операций индексирования (1-8)
- проверка верхней границы исключена только для операции 5, проверки для 2 и 3 объединены
- да, для операции 4 (
arr[0]
) на каждой итерации проверяется, чтоarr.length != 0
Очевидно, что все связанные ветки проверки предсказаны идеально, поэтому сортировка кучи с простой индексацией медленнее, чем небезопасная, только на 13%.
Я определенно подумал, что работа JIT оптимизирует связанные проверки, по крайней мере, для операций 1, 2 и 3, где индекс постоянно уменьшается от некоторого значения ниже длины массива до нуля.
Вопрос в заголовке: почему HotSpot JIT в этом случае так плохо справляется с устранением связанных проверок?
1 ответ
Я не думаю, что все латексы ограничены.
Передача массива нулевой длины приведет к IOOB. Попробуйте if (n==0) return
до цикла.