Несколько часов назад я ответил на другой вопрос о переполнении Qaru и дал очень неожиданный результат. Ответ можно найти здесь. Ответ был/частично неправильным, однако я сфокусирован на добавлении байта.
Строго говоря, на самом деле это байтовое дополнение.
Это базовый код, который я использовал:
public class ByteAdditionBenchmark {
private void start() {
int[] sizes = {
700_000,
1_000,
10_000,
25_000,
50_000,
100_000,
200_000,
300_000,
400_000,
500_000,
600_000,
700_000,
};
for (int size : sizes) {
List<byte[]> arrays = createByteArrays(size);
//Warmup
arrays.forEach(this::byteArrayCheck);
benchmark(arrays, this::byteArrayCheck, "byteArrayCheck");
}
}
private void benchmark(final List<byte[]> arrays, final Consumer<byte[]> method, final String name) {
long start = System.nanoTime();
arrays.forEach(method);
long end = System.nanoTime();
double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + " ns");
}
private List<byte[]> createByteArrays(final int amount) {
Random random = new Random();
List<byte[]> resultList = new ArrayList<>();
for (int i = 0; i < amount; i++) {
byte[] byteArray = new byte[4096];
byteArray[random.nextInt(4096)] = 1;
resultList.add(byteArray);
}
return resultList;
}
private boolean byteArrayCheck(final byte[] array) {
long sum = 0L;
for (byte b : array) {
sum += b;
}
return (sum == 0);
}
public static void main(String[] args) {
new ByteAdditionBenchmark().start();
}
}
И вот результаты, которые я получаю:
Benchmark: byteArrayCheck/итерации: 700000/время на итерацию: 50.26538857142857 ns
Контрольный показатель: byteArrayCheck/итерации: 1000/время на итерацию: 20.12 ns
Контрольный показатель: byteArrayCheck/iterations: 10000/время на итерацию: 9.1289 ns
Контрольный показатель: byteArrayCheck/итерации: 25000/время на итерацию: 10.02972 ns
Контрольный показатель: byteArrayCheck/итерации: 50000/время на итерацию: 9.04478 нс
Контрольный показатель: byteArrayCheck/iterations: 100000/время на итерацию: 18.44992 ns
Benchmark: byteArrayCheck/iterations: 200000/время на итерацию: 15.48304 ns
Контрольный показатель: byteArrayCheck/iterations: 300000/время на итерацию: 15.806353333333334 ns
Контрольный показатель: byteArrayCheck/iterations: 400000/время на итерацию: 16.923685 ns
Контрольный показатель: byteArrayCheck/итерации: 500000/время на итерацию: 16.131066 ns
Benchmark: byteArrayCheck/iterations: 600000/время на итерацию: 16.435461666666665 ns
Benchmark: byteArrayCheck/iterations: 700000/время на итерацию: 17.107615714285714 ns
Насколько мне известно, JVM уже полностью разогревается после первых 700 000 итераций, прежде чем начнет выплевывать данные бенчмаркинга.
Как же может случиться, что, несмотря на разминку, производительность все еще непредсказуема? Как почти сразу после того, как добавление байта прогрева становится невероятно быстрым, но после этого он снова сходится к номинальному 16 ns за добавление снова.
Тестирование проводилось на ПК с поддержкой Intel i7 3770 и 16 ГБ оперативной памяти, поэтому я не могу выходить за рамки 700000 итераций. Он работает на 64-разрядной версии Windows 8.1, если это имеет значение.
Оказывается, что JIT оптимизировал все, как было предложено raphw.
Поэтому я заменил эталонный метод следующим:
private void benchmark(final List<byte[]> arrays, final Predicate<byte[]> method, final String name) {
long start = System.nanoTime();
boolean someUnrelatedResult = false;
for (byte[] array : arrays) {
someUnrelatedResult |= method.test(array);
}
long end = System.nanoTime();
double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
System.out.println("Result: " + someUnrelatedResult);
System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + "ns");
}
Это гарантирует, что он не может быть оптимизирован, и результаты тестирования также покажут его (прояснить результат печати для ясности):
Контрольный показатель: byteArrayCheck/итерации: 700000/время на итерацию: 1658.2627914285715 ns
Benchmark: byteArrayCheck/iterations: 1000/время на итерацию: 1241.706 ns
Benchmark: byteArrayCheck/iterations: 10000/время на итерацию: 1215.941 ns
Benchmark: byteArrayCheck/итерации: 25000/время на итерацию: 1332.94656 ns
Контрольный показатель: byteArrayCheck/iterations: 50000/время на итерацию: 1456.0361 ns
Benchmark: byteArrayCheck/iterations: 100000/время на итерацию: 1753.26777 ns
Контрольный показатель: byteArrayCheck/итерации: 200000/время на итерацию: 1756.93283 нс
Benchmark: byteArrayCheck/iterations: 300000/время на итерацию: 1762.9992266666666 ns
Benchmark: byteArrayCheck/iterations: 400000/время на итерацию: 1806.854815 ns
Контрольный показатель: byteArrayCheck/итерации: 500000/время на итерацию: 1784.09091 ns
Контрольный показатель: byteArrayCheck/iterations: 600000/время на итерацию: 1804.6096366666666 ns
Контрольный показатель: byteArrayCheck/итерации: 700000/время на итерацию: 1811.0597585714286 ns
Я бы сказал, что эти результаты выглядят намного более убедительными в отношении времени вычислений. Однако мой вопрос все еще стоит. При повторных тестах в случайные моменты один и тот же шаблон остается тем, что тесты с меньшим количеством итераций быстрее, чем те, у которых больше итераций, хотя они, похоже, стабилизируются на 100 000 итераций или где-то ниже.
Какое объяснение?