Вздумалось чего-то мне потестить, как в
Java различные модификации кода влияют на быстродействие. Для начала написал такую вот тестирующую программку:
import java.util.*;
public class Simple_Performance_Test_Function {
private static final long seed = System .currentTimeMillis ();
private static final Random rng = new Random (seed);
private static final int size = 30000000;
private static final byte [] values = new byte [size];
private static final int iterNumber = 50;
private static final double [] times1 = new double [iterNumber];
private static final double [] times2 = new double [iterNumber];
public static void main(String[] args) {
int k, res1, res2;
long startTime, stopTime;
double time1, time2;
double [] stats1, stats2;
generateTest ();
for (k = 0; iterNumber > k; ++k) {
startTime = System .nanoTime ();
res1 = simpleTest ();
stopTime = System .nanoTime ();
time1 = 1e-6 * (stopTime - startTime);
startTime = System .nanoTime ();
res2 = functionTest ();
stopTime = System .nanoTime ();
time2 = 1e-6 * (stopTime - startTime);
times1 [k] = time1;
times2 [k] = time2;
System .out .println (String .format (Locale .UK, "%9.3f%9.3f%12d%12d", time1, time2, res1, res2));
}
stats1 = calculateStats (times1);
stats2 = calculateStats (times2);
System .out .println ();
System .out .println (String .format (Locale .UK, "%9.3f%9.3f", stats1 [0], stats2 [0]));
System .out .println (String .format (Locale .UK, "%9.3f%9.3f", stats1 [1], stats2 [1]));
}
private static void generateTest () {
int k;
for (k = 0; size > k; ++k) {
values [k] = (byte) rng .nextInt (42);
}
}
private static double [] calculateStats (double [] times) {
int k, limit;
double value, sum, sum2;
double [] result;
Arrays .sort (times);
value = times [(iterNumber - 1) / 2];
limit = Arrays .binarySearch (times, 2 * value - times [2]);
if (0 > limit) {
limit = -limit - 1;
}
sum = 0;
sum2 = 0;
for (k = 2; limit > k; ++k) {
value = times [k];
sum += value;
sum2 += value * value;
}
limit -= 2;
result = new double [2];
result [0] = sum / limit;
result [1] = Math .sqrt ((sum2 - sum * sum / limit) / (limit - 1));
return result;
}
private static int simpleTest () {
int k, result = 0;
for (k = 0; size > k; ++k) {
result += values [k];
}
return result;
}
private static int functionTest () {
int k, result = 0;
for (k = 0; size > k; ++k) {
result += testFunction (k);
}
return result;
}
private static byte testFunction (int index) {
return values [index];
}
}
Объектом тестирования в ней являются функции
simpleTest () и
functionTest (). Каждая из них суммирует элементы заранее сгенерированного массива
values случайных чисел. Разница лишь в том, что первая процедура суммирует значения сразу, а вторая для получения очередного значения из массива вызывает вложенную функцию
testFunction (int index). Идея теста заключается в том, чтобы проверить как много времени требуется, чтобы организовать вызов функции.
К моему удивлению время работы функций
simpleTest () и
functionTest () оказалось одинаковым и равным
Код:
14.739 14.666
0.176 0.153
Видимо, JIT-компилятор сообразил, что вложенную функцию во втором цикле можно заинлайнить.
Дальше я сделал такую модификацию:
private static int simpleTest () {
int k, result = 0;
for (k = 0; size > k; ++k) {
if (0 == (values [k] & 1)) {
result += values [k];
}
}
return result;
}
private static int functionTest () {
int k, result = 0;
for (k = 0; size > k; ++k) {
result += testFunction (k);
}
return result;
}
private static byte testFunction (int index) {
if (0 == (values [index] & 1)) {
return values [index];
}
return 0;
}
Добавилось условие на необходимость добавления очередной величины к результату. Время работы разумеется возросло, причём больше, чем я ожидал: проверка условия (по идее) делается порядка времени сложения, а сложение теперь происходит в среднем в два раза реже (так как величины в массиве
values — случайные). Ожидаемый рост времени работы не более чем в полтора раза (ведь там же ещё есть обслуживание цикла и чтение массива). Время же выросло более чем в два раза, и процедура
functionTest () оказалась хуже:
Код:
36.488 40.319
0.305 0.317
Поэкспериментировав я заметил, что если в первой функции
simpleTest () равенство в условии заменить не неравенство:
if (0 != (values [k] & 1)), то время работы стабильно вырастает на 2 миллисекунды и становится:
Код:
38.392 39.974
0.244 0.154
На время работы второй функции такая же замена почему-то не влияет. В любом случае, разница во времени работы не на столько велика, чтобы сделать заключение, что компилятор перестал инлайнить вложенную функцию второй процедуры. Однако сложение в ней однозначно производится на каждой итерации цикла. Компилятор не сообразил, что его можно не делать, когда условие выбора не выполняется.
Дальше я сделал такой апгрейд:
private static int simpleTest () {
int k, result = 0;
byte value;
for (k = 0; size > k; ++k) {
value = values [k];
if (0 == (value & 1)) {
result += value;
}
}
return result;
}
private static int functionTest () {
int k, result = 0;
for (k = 0; size > k; ++k) {
result += testFunction (k);
}
return result;
}
private static byte testFunction (int index) {
byte value = values [index];
if (0 == (value & 1)) {
return value;
}
return 0;
}
Его идея заключается в том, чтобы проверить, умеет ли компилятор замечать подряд идущие обращения к одному и тому же элементу массива. Оказалось, что не умеет: время выполнения резко упало. Это объясняет, почему во второй модификации при добавлении проверки время выполнения возросло. Однако! Для процедуры
functionTest () время упало слишком резко! Она стала работать почти в два раза быстрее:
Код:
31.675 21.743
0.193 0.107
Вроде бы процедуры делают одно и то же, причём первая делает это чуть-чуть оптимальнее: она не всегда производит суммирование. Но нет! Практика показывает, что вторая процедура почти в полтора раза эффективнее.
В этих казалось бы элементарных тестах я получил совершенно невероятные на мой неискушённый взгляд результаты. В связи с этим, у меня естественным образом возникает вопрос:
а почему происходит то, что происходит? Можно ли хоть как-то объяснить подобное поведение времени работы?