4 декабря 2016 г.

Так что насчет производительности?..

Значительная часть моих историй про скаляризацию состоит из описания того, как все весело разваливается, чуть что-нибудь где-нибудь недозаинлайнилось. После чего у некоторых читателей возникает ощущение "да ну его в жопу, я лучше сам все закэширую, и все гарантированно будет быстро, и не важно, что там как инлайнится". Честно говоря, мне и самому стало интересно, насколько оно все будет быстро, если плохо заинлайнится, поэтому я написал такой несложный бенчмарк:
public class GCCostBenchmark {


 @Param( { "32", "64", "128", "256", "1024" } )
 public static int SIZE = 128;

 ArrayList<Integer> list = new ArrayList<>();

 @Setup
 public void setup() {
  final ThreadLocalRandom rnd = ThreadLocalRandom.current();
  for( int i = 0; i < SIZE; i++ ) {
   list.add( rnd.nextInt() );
  }
 }


 @Benchmark
 public long sumWithIterator() {
  long sum = 0;
  for( final Integer n : list ) {
   sum += n;
  }
  return sum;
 }

 @Benchmark
 public long sumWithIndex() {
  long sum = 0;
  for( int i = 0; i < list.size(); i++ ) {
   final Integer n = list.get( i );
   sum += n;
  }
  return sum;
 }
}
Просто создаем ArrayList разных размеров, заполняем его случайными числами, и в цикле суммируем. И смотрим на два разных варианта организации цикла: через итератор, и старперский, через индекс.
Benchmark               (SIZE)   Mode  Cnt   Score    Error   Units
sumWithIndex                32  thrpt    5  27.195 ±  3.476  ops/us
sumWithIndex                64  thrpt    5  15.362 ±  0.443  ops/us
sumWithIndex               128  thrpt    5   8.359 ±  0.775  ops/us
sumWithIndex               256  thrpt    5   4.243 ±  0.268  ops/us
sumWithIndex              1024  thrpt    5   1.115 ±  0.029  ops/us
sumWithIterator             32  thrpt    5  24.300 ±  0.244  ops/us
sumWithIterator             64  thrpt    5  12.973 ±  0.056  ops/us
sumWithIterator            128  thrpt    5   7.415 ±  0.035  ops/us
sumWithIterator            256  thrpt    5   4.023 ±  0.392  ops/us
sumWithIterator           1024  thrpt    5   1.138 ±  0.012  ops/us
Видно, что на моей JDK 1.8.0_102 они идут ноздря в ноздрю: вариант с индексом слегка обходит итератор, но различие в пределах погрешностей. Ок, это будет наша реперная точка. Теперь мы запускаем тот же бенчмарк, но с флагом -XX:-EliminateAllocations. В у бенчмарка с итератором, разумеется, появляются строчки gc-profiler-а gc.alloc.rate.norm=32.000±0.001 B/op, но меня интересуют другие цифры:
Benchmark                (SIZE)   Mode  Cnt    Score    Error   Units
sumWithIndex                 32  thrpt    5   27.063 ±  1.527  ops/us
sumWithIndex                 64  thrpt    5   15.571 ±  0.243  ops/us
sumWithIndex                128  thrpt    5    7.795 ±  0.066  ops/us
sumWithIndex                256  thrpt    5    4.213 ±  0.022  ops/us
sumWithIndex               1024  thrpt    5    1.120 ±  0.011  ops/us
sumWithIterator              32  thrpt    5   21.022 ±  1.452  ops/us
sumWithIterator              64  thrpt    5   11.295 ±  2.082  ops/us
sumWithIterator             128  thrpt    5    6.145 ±  0.273  ops/us
sumWithIterator             256  thrpt    5    3.359 ±  0.035  ops/us
sumWithIterator            1024  thrpt    5    0.905 ±  0.032  ops/us
Как и можно было бы ожидать, итерация с индексом почти никак не отреагировала на этот флаг, а итератор стал медленнее, примерно на ~15-20%. Но это я волевым решением отрезал скаляризацию. А давайте сэмулируем отсутствие инлайнинга (и как следствие — скаляризации):
-XX:CompileCommand="dontinline,java.util.AbstractList::*"
-XX:CompileCommand="dontinline,java.util.ArrayList::*"
— я отключаю инлайнинг для всех методов ArrayList/AbstractList (ну, чтобы уж с надежностью):
Benchmark                (SIZE)   Mode  Cnt    Score    Error   Units
sumWithIndex                 32  thrpt    5    2.767 ±  0.033  ops/us
sumWithIndex                 64  thrpt    5    1.410 ±  0.012  ops/us
sumWithIndex                128  thrpt    5    0.709 ±  0.003  ops/us
sumWithIndex                256  thrpt    5    0.357 ±  0.003  ops/us
sumWithIndex               1024  thrpt    5    0.093 ±  0.003  ops/us
sumWithIterator              32  thrpt    5    3.528 ±  0.055  ops/us
sumWithIterator              64  thrpt    5    1.821 ±  0.029  ops/us
sumWithIterator             128  thrpt    5    0.933 ±  0.015  ops/us
sumWithIterator             256  thrpt    5    0.464 ±  0.020  ops/us
sumWithIterator            1024  thrpt    5    0.119 ±  0.003  ops/us
Без инлайнинга производительность цикла просела в 10 раз! (кстати, теперь версия с итератором работает быстрее :)

Пример немного искусственный: уж больно много разных loop optimizations отваливается без инлайнинга. Для кода более общего вида настолько радикального падения производительности не будет. Но в целом, мораль истории такова: если у вас где-то в горячем коде не сработал инлайнинг — у вас скорее всего уже достаточно серьезные проблемы с производительностью в этом месте. Не скаляризованные аллокации добавят сюда лишь какие-то крохи.