Статьи

Встроенный лямбда-выражение 8

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

Вслед за этим компактные профили обещают открыть огромные преимущества совместимости Java Standard Edition со встроенными платформами, которые ранее считались слишком маленькими. Вы видите, куда это идет? Может быть интересно использовать эти две технологии одновременно и посмотреть, насколько хорошо они работают вместе. Далее следует описание маленькой программы и ее показателей производительности — если хотите, микробенчмарка, цель которого — показать, как программирование с новой парадигмой Lambda Expression может быть полезным не только для типичных настольных компьютеров и серверов, но и для растущей количество встроенных платформ тоже.

Платформа аппаратного обеспечения / ОС

Основной интерес для этой статьи представляет
одноплатный компьютер
Boundary Devices BD-SL-i.MX6 . Это четырехъядерная система на базе ARM® Cortex ™ -A9 с 1 ГБ ОЗУ, работающая под
управлением   дистрибутива Debian Linux. На момент публикации этой статьи ее прейскурантная цена составляла 199 долларов США.

Что делает его более интересным, так это то, что мы будем не только запускать лямбда-выражения Java 8 на устройстве, мы будем делать это в рамках нового профиля Java 8 Compact1.  Статический размер этой среды выполнения Java составляет 10½ МБ.

Вторая система, полностью отличающаяся по своим возможностям и возможностям от нашего встроенного устройства, будет использоваться в качестве средства для сравнения и сопоставления поведения при выполнении в разных средах оборудования и ОС. Речь идет о ноутбуке Toshiba Tecra R840 с операционной системой Windows 7/64-bit. Он имеет двухъядерный процессор Intel® Core ™ i5-2520M с 8 ГБ ОЗУ и будет использовать стандартную среду исполнения Java 8 (JRE) для 64-битной Windows.

Приложение

Ища образец набора данных в качестве основы для нашего элементарного приложения, эта ссылка предоставляет идеальную (и вымышленную) базу данных о сотрудниках. Среди доступных форматов CSV-файл, разделенный запятыми, содержит приблизительно 300 000 записей. Наш пример приложения прочитает этот файл и сохранит записи сотрудников в LinkedList <EmployeeRec>. EmployeeRec имеет следующие поля:

    public class EmployeeRec {
        private String id;
        private String birthDate;
        private String lastName;
        private String firstName;
        private String gender;
        private String hireDate;
        ...
    }

После инициализации этой структуры данных нашему приложению предлагается выполнить одну простую задачу:  вычислить средний возраст всех сотрудников мужского пола .

Старая школа

Во-первых, давайте выполним этот расчет так, чтобы он предшествовал доступности лямбда-выражений. Мы назовем эту версию OldSchool . Код, выполняющий вычисление «среднего возраста всех сотрудников мужского пола», выглядит следующим образом:

double sumAge = 0;
long numMales = 0;
for (EmployeeRec emp : employeeList) {
    if (emp.getGender().equals("M")) {
        sumAge += emp.getAge();
        numMales += 1;
    }
}
double avgAge = sumAge / numMales;

Лямбда-выражение Версия 1

Наш второй вариант будет использовать лямбда-выражение для выполнения идентичных вычислений. Мы назовем эту версию лямбда-потоком () . Ключевое утверждение в Java 8 выглядит следующим образом:

double avgAge = employeeList.stream()
                .filter(s -> s.getGender().equals("M"))
                .mapToDouble(s -> s.getAge())
                .average()
                .getAsDouble();

Лямбда-выражение, версия 2

В нашем последнем варианте используется предыдущее лямбда-выражение с одной небольшой модификацией: он заменяет вызов метода stream () на метод parallelStream (), предлагая возможность разбить задачу на более мелкие блоки, работающие в отдельных потоках. Мы назовем эту версию Lambda parallelStream () . Инструкция Java 8 выглядит следующим образом:

double avgAge = employeeList.parallelStream()
                .filter(s -> s.getGender().equals("M"))
                .mapToDouble(s -> s.getAge())
                .average()
                .getAsDouble(); 

Начальные результаты испытаний

Диаграммы, которые следуют, показывают время выполнения примера задачи, решенной с помощью наших трех вышеупомянутых вариантов. Левая диаграмма представляет время, записанное на процессоре ARM Cortex-A9, в то время как правая диаграмма показывает записанное время для Intel Core-i5. Чем меньше результат, тем быстрее оба примера указывают на то, что использование последовательного лямбда-потока () в дополнение к старому школьному до-лямбда-решению имеет некоторые издержки. Что касается ParallelsStream (), то это смешанный пакет. Для Cortex-A9 операция parallelStream () пренебрежимо быстрее, чем решение старой школы, в то время как для Core-i5 накладные расходы, связанные с функцией ParallelsStream (), на самом деле замедляют решение.

Без дальнейшего изучения можно сделать вывод, что параллельные потоки могут не стоить усилий. Но что, если выполнить тривиальный расчет для списка из 300 000 сотрудников просто недостаточно, чтобы продемонстрировать преимущества распараллеливания? В следующей серии тестов мы увеличим вычислительную нагрузку, чтобы увидеть, как может быть повышена производительность.

Добавление дополнительной работы к тесту

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

/*
 * Rube Goldberg way of calculating identity of 'val',
 * assuming number is positive
 */
private static double identity(double val) {
    double result = 0;
    for (int i=0; i < loopCount; i++) {
        result += Math.sqrt(Math.abs(Math.pow(val, 2)));    
    }
    return result / loopCount;
}

Поскольку этот метод берет квадратный корень из квадрата числа, он по сути является дорогой функцией тождества. Изменяя значение loopCount (это делается с помощью параметра командной строки), мы можем изменить количество раз, которое этот цикл выполняет за вызов identity (). Этот метод вставляется в наш код, например, с помощью версии Lambda ParallelStream (), следующим образом:

double avgAge = employeeList.parallelStream()
                .filter(s -> s.getGender().equals("M"))
                .mapToDouble(s -> identity(s.getAge()))
                .average()
                .getAsDouble();

Модификация, идентичная выделенной красным цветом, также применяется к вариантам Old School и Lambda Stream (). Диаграммы, которые следуют, показывают время выполнения для трех отдельных запусков нашего микробенчмарка, каждый из которых имеет свое значение, присвоенное внутренней переменной loopCount в нашей функции Rube Goldberg identity ().

Для Cortex-A9 вы можете отчетливо видеть преимущество в производительности ParallelsStream (), когда счетчик циклов установлен на 100, и он становится еще более поразительным, когда счетчик циклов увеличивается до 500. Для Core-i5 требуется много больше работы, чтобы реализовать преимущества функции ParallelsStream (). До тех пор, пока счетчик циклов не будет установлен на 50 000, преимущества в производительности станут очевидными. Core-i5 намного быстрее и имеет только два ядра; следовательно, количество усилий, необходимых для преодоления начальных издержек функции ParallelsStream (), намного более существенно.

Загрузки

Пример кода, используемый в этой статье, доступен в виде проекта NetBeans. Поскольку проект включает CSV-файл с более чем 300 000 записей, он больше, чем можно было бы ожидать. На   сайте blogs.oracle.com запрещено хранить файлы размером более 2 МБ, поэтому этот источник проекта был сжат и разделен на три части. Вот ссылки:

Просто объедините три загруженных файла вместе, чтобы воссоздать оригинальный файл LambdaMicrobench.zip. В Linux команда будет выглядеть примерно так:

 $ cat LambdaMicrobench.zip.part? > LambdaMicrobench.zip

Вывод

Java 8 приложила немало усилий, чтобы сделать ее более универсальной платформой. Наш простой пример демонстрирует, что даже встроенная среда выполнения Java размером всего 10½ МБ может использовать преимущества последних достижений Java. Это только начало. Предстоит проделать еще много работы для дальнейшего улучшения характеристик производительности лямбда-выражений в параллельном потоке. Мы с нетерпением ждем будущих улучшений.