Java 8 Lambda vs LambdaJ vs Guava vs Iterative approach

    В середине 2013 года выходит Java 8 с реализацией лямбда-выражений. Лямбда-выражения предоставляют широкие возможности для применения функционального стиля программирования. Правда функциональный стиль можно использовать уже сегодня в JDK 7, 6 или 5 с помощью библиотек LambdaJ и Guava.
    Iterative Lambdaj JDK 8 lambda Guava
    Print all brands 79 472* 113 79
    Select all sales of a Ferrari 25 146 44 31
    Find buys of youngest person 1,209 1,775 1,242 1,218
    Find most costly sale 8 123 55 72
    Sum costs where both are males 23 973* 40 45
    Age of youngest who bought for > 50,000 1,521 2,576* 1,560 1,511
    Sort sales by cost 350 1,187 473 453
    Extract cars original cost 29 61 31 31
    Index cars by brand 57 70 89 415
    Group sales by buyers and sellers 2,586 3,748* 2,862 1,601
    Find most bought car 744 1,023* CRASH 1,435

    Guava — достаточно обширная библиотека, но её ядро составляют классы для работы с коллекциями, собственно библиотека и выросла из google-collections. Эти классы реализуют всё то для работы с коллекциями, чего не хватает в JDK на сегодняшний день. Если приглядеться к JDK 8, то постоянно возникает ощущение, что та или иная новая возможность уже есть в guava. Не исключено, что разработчики восьмой джавы опирались именно на опыт гуавы.

    LamdaJ, несмотря на свое название, не предоставляет возможности писать лямбда-выражения, это скорее библиотека для работы с коллекциями. Она дает возможности фильтровать, конвертировать, группировать элементы коллекций, при этом код получается очень компактный и хорошо читается. Она реализована средствами reflection и сglib, и очевидно, что за удобство приходится платить производительностью. О LambdaJ уже писали на хабре.

    В JDK 8 лямбда-выражения будут реализованы уже на уровне компилятора и будут преобразовываться в байткод еще на этапе компиляции, поэтому теоретически производительность такого кода должна быть выше.

    Сравнение производительности


    Для того чтобы практически сравнить производительность лямбда-выражений из JDK 8 с LambdaJ, Guava и обычным итеративным подходом, были взяты тесты производительности LambdaJ и написаны аналоги на JDK 8 и Guava.

    Изначальный код тестов для итеративного варианта и LambdaJ тронут не был, названия тестов сохранены на английском во избежание путаницы. Методика тестирования следующая: тесты запускаются в 100 проходов, где каждый тест запускается 100000 раз, замеряется общее время выполнения каждого прохода и усредняется.

    Не все LambdaJ тесты запустились в JRE 8, в таблице они помечены звездочкой, результаты для них измерялись в JRE 7. В одном тесте лямбда-выражение уронило виртуальную машину совсем.

    Тесты исполнялись на компьютере со следующей конфигурацией: Core i5 760 2.8 GHz 8 GB Win 7 sp-1 64 bit lambda-8-b45-windows-x64-24_jun_2012 jdk-7u4-windows-x64.

    В итоге самые лучшие результаты дали итеративный код, Guava и JDK лямбды. Они дают вполне соизмеримые результаты. В общем-то можно сказать, что лямбда-выражения предоставляют вполне удовлетворительную замену итерациям и анонимным классам. И JDK 8, и Guava, и итеративный подход дают очень похожие результаты, но LamdaJ отстает чуть ли не на порядок, причина скорее всего кроется в широком использовании reflection.

    Следует отметить, что у Guava и у JDK лямбд есть одна особенность при работе с коллекциями. Они часто возвращают не скопированную коллекцию, а её live view, которое откладывает вычисления до момента обращения к элементам этого view. Поэтому в тестах все live view были явно преобразованы в коллекции. Иначе некоторые тесты давали бы результат практически в 0 миллисекунд. Еще нужно сказать, что в реальной жизни никто не требует делать выбор в пользу какого либо подхода, например Guava будет отлично сочетаться с лямбда-выражениями.

    Пример


    Для примера как выглядит исходный код для каждого из подходов можно вглянуть на тест FindAgeOfYoungestWhoBoughtForMoreThan50000Test. В этом тесте дан список продаж автосалона, и в этом списке находится возраст самого молодого покупателя сделавшего покупку больше чем за 50000 каких-нибудь денег.

    Итеративный подход

    int age = Integer.MAX_VALUE;
    for (final Sale sale : db.getSales()) {
        if (sale.getCost() > 50000.00) {
            final int buyerAge = sale.getBuyer().getAge();
            if (buyerAge < age) {
                age = buyerAge;
            }
        }
    }
    

    LambdaJ

    final int age = Lambda.min(forEach(select(db.getSales(), having(on(Sale.class).getCost(),
                        greaterThan(50000.00)))).getBuyer(), on(Person.class).getAge());
    

    JDK 8 lambda

    final int age = Collections.min(db.getSales()
                        .filter((Sale sale)->sale.getCost() > 50000.00)
                        .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                        .into(new ArrayList<Integer>()));
    

    Guava

    final int age = Collections.min(transform(filter(db.getSales(), new Predicate<Sale>() {
                    @Override
                    public boolean apply(final Sale input) {
                        return input.getCost() > 50000.00;
                    }
                }), new Function<Sale, Integer>() {
                    @Override
                    public Integer apply(final Sale input) {
                        return input.getBuyer().getAge();
                    }
                }));
    


    Ссылки




    Update


    По совету TheShade я внес изменения в процедуру тестирования: добавил прогрев и подсчет доверительных интервалов. Результаты изменились следующим образом:
    Iterative Lambdaj JDK 8 lambda Guava
    Print all brands 797.3 4,436.6* 1,073.2 912.1
    Select all sales of a Ferrari 471.1 1,359.6 398.5 341.7
    Find buys of youngest person 12,102.4 17,436.2 12,267.4 12,144.5
    Find most costly sale 89.7 1,237.2 742.8 676.6
    Sum costs where both are males 173.7 8,979.8* 372.7 394.5
    Age of youngest who bought for > 50,000 11,971.2 28,091.9* 11,672.1 15,975.6
    Sort sales by cost 3,261 12,566.1 4,558.8 3,242.5
    Extract cars original cost 292.1 628 322.8 213.1
    Index cars by brand 548.8 672.1 867.8 4,324.6
    Group sales by buyers and sellers 25,986.3 39,339.3* 28,109.2 15,068.9
    Find most bought car 7,378.3 9,317.2* CRASH 10,002.1
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 36

      +1
      В JDK 8 лямбда-выражения будут реализованы уже на уровне компилятора и будут преобразовываться в байткод еще на этапе компиляции, поэтому теоретически производительность такого кода должна быть выше.
      Не очень понятно. А в остальных озвученных проектах они что, на этапе выполнения что ли транслируются и выполняются? O_o Очевидно же, что и в JDK8 лямбды и прочее — всего лишь сахар и развернутся на этапе компиляции в какие-нибудь обычные существующие конструкции, в этом же виде они и будут представлены байт-кодом. В случае лямбд, емнип, это будут просто статические методы.
        +2
        Вот, собственно, в этой презентации в частности про проект лямбда вкратце сказано: cr.openjdk.java.net/~jrose/pres/201204-LangNext.pdf
          0
          А в остальных озвученных проектах они что, на этапе выполнения что ли транслируются и выполняются?

          В случае LambdaJ это так, собственно поэтому эта библиотека оказалась самая медленная.
            0
            А, ну ок. Я не пользовался LambdaJ, могу ошибаться, но судя по коду
            final int age = Lambda.min(forEach(select(db.getSales(), having(on(Sale.class).getCost(),
                                greaterThan(50000.00)))).getBuyer(), on(Person.class).getAge());
            всё же не транслируется уж наверно на этапе выполнения, просто через рефлекшен работает.
            0
            > Очевидно же, что и в JDK8 лямбды и прочее — всего лишь сахар и развернутся на этапе компиляции в какие-нибудь обычные существующие конструкции

            Вообще-то не очевидно. Если я не ошибаюсь, на некотором этапе разработки спецификации JDK7/8 обсуждался вариант модифицировать байт-код JVM таким образом, чтобы были возможны настоящие замыкания переменных на стеке(в текущей реализации всех этих гуав замыкание происходит через кучу).
          • UFO just landed and posted this here
              0
              хотелось бы узнать, зачем в реализации лямбда-выражений опираться на динамические узлы вызовов?
              ведь после компиляции, по-моему, модифицированный «компилятор» должен сгенерировать идентичный код, как если бы это происходило на этапе компиляции.

              >Это даст возможность генерировать оптимальный код для лямбд уже после javac-компиляции

              я так понимаю, что лямбды являются статически типизированными с сильной типизацией, тогда что может дать динамизм?
              • UFO just landed and posted this here
                  0
                  спасибо за ответ )
            • UFO just landed and posted this here
                0
                Замечания конструктивные.

                1. Согласен, вообще iterable, guava и jdk8 в большинстве тестов дают результаты очень близкие
                2. Это я поправлю
                3. Увы, все тесты запускались в одной JVM, исходник тут
                4. Усреднился
                • UFO just landed and posted this here
                0
                А функциональный стиль разбрасывается по ядрам или в один поток фигачит?
                  0
                  В данных тестах в один поток. В JDK 8 заложены возможности вызывать некоторые вещи параллельно, например в интерфейсе List появился метод parallelSort

                  public void parallelSort(Comparator<? super E> c) default {
                      Collections.<E>sort(this,c);
                  }
                  
                    0
                    Вот например код:
                    final int age = db.getSales()
                                        .filter((Sale sale)->sale.getCost() > 50000.00)
                                        .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                                        .reduce(0, (x,y)-> x<y ? x : y);
                    

                    сейчас он шпарит (и будет шпарить) в один тред.
                    Но! Будет магический метод parallel (еще не сделан), и можно будет написать так:
                    final int age = db.getSales()
                                        .parallel()
                                        .filter((Sale sale)->sale.getCost() > 50000.00)
                                        .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                                        .reduce(0, (x,y)-> x<y ? x : y);
                    
                      0
                      а можно так:
                      final int age = db.getSales()
                                          .filter((Sale sale)->sale.getCost() > 50000.00)
                                          .parallel()
                                          .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                                          .reduce(0, (x,y)-> x
                      

                      Тогда фильтр последовательный, а мап и редьюс параллельные.
                      0
                      Было бы интересно добавить в сравнение другие JVM языки с lambda, например Scala
                        0
                        Тест «Index cars by brand» похоже некорректен.
                        Guava заметно отстает от Iterative и если посмотреть на код теста, то в версии для Guava вместо Map<String, Car> используется ImmutableListMultimap<String, Car>, который хранит для ключа коллекцию значений. Возможно в этом и причина отставания.
                          0
                          Да, вероятно вы правы. У меня не получилось сделать Guava реализацию полностью идентичной.
                            0
                            Результат теста исходя из названия должен возвращать что-то вроде Map<String, Iterable<Car>>. Для Guava это соблюдается, для остальных вариантов одному бренду будет соответствовать только одна машина. Предлагаю либо исключить этот тест, либо написать правильные реализации для других вариантов. Сейчас он не дает объективного сравнения.
                          0
                          Вас не смущает, что в тесте на самую крупную сделку, в официальном тесте по ссылке разница производительности в 3 раза, а у вас — в 15?
                            0
                            Да, это смущает, я специально несколько раз проверял результаты вычислений и не нашел никаких изъянов.
                            0
                            Петька, приборы?
                            Что означают числа в таблицах?
                              0
                              И когда компиляете тест для JDK8 lambda — поставте javac ключик: -XDlambdaToMethod
                                0
                                WARMUP_ITERATIONS_COUNT = 10;
                                too small.
                                etc.
                                  0
                                  впрочем, тут я спешу. Может и хватит. Но надо доказать, что хватит.
                                  • UFO just landed and posted this here
                                      0
                                      не мешайте, сэр. У меня можно сказать ломка на отдыхе, а тут тааакое ;)
                                      • UFO just landed and posted this here
                                          0
                                          мм, я так понимаю, это междусобойчик программистов из oracle? добрый вечер всем, добро пожаловать на вечеринку
                                            0
                                            Сейчас, я ни за что не ответственный. ;) Я в отпуске. ;)
                                    0
                                    Кстати, а лямбды то тут причем? ;) Сравнивается то, что называется «collections bulk operations». :)
                                      0
                                      вы правы, можно было и без лямбд обойтись
                                      0
                                      Кстати, это плохо:
                                      final int age = Collections.min(db.getSales()
                                                          .filter((Sale sale)->sale.getCost() > 50000.00)
                                                          .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                                                          .into(new ArrayList<Integer>()));
                                      


                                      Надо:
                                      final int age = db.getSales()
                                                          .filter((Sale sale)->sale.getCost() > 50000.00)
                                                          .<Integer>map((Sale sale)->sale.getBuyer().getAge())
                                                          .reduce(0, (x,y)-> x<y ? x : y);
                                      
                                      


                                        0
                                        Я бы даже сказал как-то так:
                                        final int age = db.getSales()
                                                            .filter(sale->sale.getCost() > 50000.00)
                                                            .mapToInt(sale->sale.getBuyer().getAge())
                                                            .min()
                                                            .orElse(0);
                                        

                                        (сорри за некропост)
                                        +1
                                        В Гуаве есть примочка, которая позволяет писать код практически неотличимый от JDK8 лямбд. Возможно потом появится и какой-то автоматичский переход. Примочка вот: docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/FluentIterable.html

                                        Only users with full accounts can post comments. Log in, please.