Я хочу понять, какие оптимизации Java делает для последовательных циклов. Точнее, я пытаюсь проверить, выполняется ли слияние фьюжн. Теоретически я ожидал, что эта оптимизация не была выполнена автоматически и ожидала подтверждения того, что плавная версия была быстрее, чем версия с двумя циклами.
Однако после запуска тестов результаты показывают, что две отдельные (и последовательные) контуры быстрее, чем один цикл, выполняющий всю работу.
Я уже пробовал использовать JMH для создания тестов и получения тех же результатов.
Я использовал команду javap
и это показывает, что сгенерированный байт-код для исходного файла с двумя циклами фактически соответствует двум выполняемым javap
(не было развернуто циклов или другая оптимизация).
Код, измеряемый для BenchmarkMultipleLoops.java
:
private void work() {
List<Capsule> intermediate = new ArrayList<>();
List<String> res = new ArrayList<>();
int totalLength = 0;
for (Capsule c : caps) {
if(c.getNumber() > 100000000){
intermediate.add(c);
}
}
for (Capsule c : intermediate) {
String s = "new_word" + c.getNumber();
res.add(s);
}
//Loop to assure the end result (res) is used for something
for(String s : res){
totalLength += s.length();
}
System.out.println(totalLength);
}
Код, измеряемый для BenchmarkSingleLoop.java
:
private void work(){
List<String> res = new ArrayList<>();
int totalLength = 0;
for (Capsule c : caps) {
if(c.getNumber() > 100000000){
String s = "new_word" + c.getNumber();
res.add(s);
}
}
//Loop to assure the end result (res) is used for something
for(String s : res){
totalLength += s.length();
}
System.out.println(totalLength);
}
И вот код для Capsule.java
:
public class Capsule {
private int number;
private String word;
public Capsule(int number, String word) {
this.number = number;
this.word = word;
}
public int getNumber() {
return number;
}
@Override
public String toString() {
return "{" + number +
", " + word + '}';
}
}
caps
- это ArrayList<Capsule>
с 20 миллионами элементов, заполненных таким образом в начале:
private void populate() {
Random r = new Random(3);
for(int n = 0; n < POPSIZE; n++){
int randomN = r.nextInt();
Capsule c = new Capsule(randomN, "word" + randomN);
caps.add(c);
}
}
Перед измерением выполняется фаза прогрева.
Я запускал каждую из тестов 10 раз или, другими словами, метод work()
выполняется по 10 раз для каждого теста, а среднее время для завершения представлено ниже (в секундах). После каждой итерации GC исполнялся вместе с несколькими спит:
- MultipleLoops: 4.9661 секунд
- SingleLoop: 7.2725 секунд
OpenJDK 1.8.0_144 работает на Intel i7-7500U (озеро Каби).
Почему версия MultipleLoops быстрее, чем версия SingleLoop, хотя она должна пересекать две разные структуры данных?
ОБНОВЛЕНИЕ 1:
Как было предложено в комментариях, если я изменяю реализацию для вычисления totalLength
при totalLength
строк, избегая создания списка res
, версия с одним циклом становится быстрее.
Однако эта переменная была введена только для того, чтобы некоторая работа была выполнена после создания списка результатов, чтобы избежать отбрасывания элементов, если с ними ничего не было сделано.
Другими словами, предполагаемый результат - составить окончательный список. Но это предложение помогает лучше понять, что происходит.
Результаты:
- MultipleLoops: 0,9339 секунд
- SingleLoop: 0.66590005 секунд
ОБНОВЛЕНИЕ 2:
Вот ссылка на код, который я использовал для теста JMH: https://gist.github.com/FranciscoRibeiro/2d3928761f76e4f7cecfcfcdf7fc96d5
Результаты:
- MultipleLoops: 7.397 секунд
- SingleLoop: 8.092 секунд