Статьи

Стоимость лени

Недавно у меня возник спор с коллегами по поводу штрафа за ленивые игры в Scala. Это привело к набору микробенчмарков, которые сравнивают ленивую и не ленивую производительность. Все источники можно найти по адресу http://git.io/g3WMzA .

Но прежде чем перейти к результатам теста, давайте попробуем понять, что может привести к снижению производительности.

Для моего теста JMH я создал очень простой класс Scala с ленивым значением val:

 
@State(Scope.Benchmark)
class LazyValCounterProvider {
  lazy val counter = SlowInitializer.createCounter()
}

Теперь давайте посмотрим, что скрыто под капотом ленивых ключевых слов. Сначала нам нужно скомпилировать данный код с помощью scalac , а затем он может быть декомпилирован в соответствующий код Java. Для этого я использовал декомпилятор JD . Это произвело следующий код:

 
@State(Scope.Benchmark)
@ScalaSignature(bytes="...")
public class LazyValCounterProvider {
  private SlowInitializer.Counter counter;
  private volatile boolean bitmap$0;

  private SlowInitializer.Counter counter$lzycompute() {
    synchronized (this) { 
      if (!this.bitmap$0) { 
        this.counter = SlowInitializer.createCounter(); 
        this.bitmap$0 = true; 
      } 
      return this.counter; 
    }  
  } 

  public SlowInitializer.Counter counter() { 
    return this.bitmap$0 
      ? this.counter 
      : counter$lzycompute(); 
  }
}
 

Как видно, ленивое ключевое слово переводится в классическую идиому блокировки с двойной проверкой для отложенной инициализации.

Таким образом, большую часть времени единственное ухудшение производительности может исходить от одного энергозависимого чтения на чтение ленивого val (за исключением времени, которое требуется для инициализации экземпляра lazy val с момента его самого первого использования). Давайте наконец измерим его влияние в цифрах.

Мой микробенчмарк на основе JMH прост:

 
public class LazyValsBenchmarks {

  @Benchmark
  public long baseline(ValCounterProvider eagerProvider) 
  {
    return eagerProvider.counter().incrementAndGet();
  }

  @Benchmark
  public long lazyValCounter(LazyValCounterProvider provider) 
  {
    return provider.counter().incrementAndGet();
  }
}
 

Базовый метод обращается к конечному объекту счетчика и увеличивает целочисленное значение на 1, вызывая incrementAndGet.

И, как мы только что выяснили, основной метод эталонного теста — lazyValCounter — в дополнение к тому, что делает метод базового уровня, также выполняет одно энергозависимое чтение.

Примечание: все измерения выполняются на MBA с процессором Core i5 1,7 ГГц.

Все результаты были получены при запуске JMH в режиме пропускной способности. Столбцы с оценками и ошибками в оценках показывают количество операций в секунду Каждый прогон JMH делал 10 итераций и занимал 50 секунд. Я выполнил 6 измерений с различными вариантами JVM и JMH:

  1. клиентская ВМ, 1 поток

     
    Benchmark                   Score    Score error    
    baseline            412277751.619    8116731.382    
    lazyValCounter      352209296.485    6695318.185 
     
    
  2. клиентская ВМ, 2 потока

     
    Benchmark                   Score    Score error    
    baseline            542605885.932   15340285.497    
    lazyValCounter      383013643.710   53639006.105 
     
    
  3. клиентская ВМ, 4 потока

     
    Benchmark                   Score    Score error    
    baseline            551105008.767    5085834.663   
    lazyValCounter      394175424.898    3890422.327
     
    
  4. серверная виртуальная машина, 1 поток

     
    Benchmark                   Score    Score error    
    baseline            407010942.139    9004641.910    
    lazyValCounter      341478430.115   18183144.277  
     
    
  5. серверная виртуальная машина, 2 потока

     
    Benchmark                   Score    Score error    
    baseline            531472448.578   22779859.685    
    lazyValCounter      428898429.124   24720626.198    
     
    
  6. серверная виртуальная машина, 4 потока

     
    Benchmark                   Score    Score error  
    baseline            549568334.970   12690164.639  
    lazyValCounter      374460712.017   17742852.788 
     
    

Цифры показывают, что штраф за выполнение ленивых вальсов довольно мал и может быть проигнорирован на практике.

Для дальнейшего прочтения этой темы я бы порекомендовал SIP 20 — Улучшенная инициализация Lazy Vals , которая содержит очень интересный углубленный анализ существующих проблем с реализацией отложенной инициализации в Scala.