Статьи

Безотходное кодирование

В этой статье описывается решение существенной проблемы обработки событий с высокой эффективностью за счет уменьшения потерь в программном стеке.

Java часто рассматривается как проблема с памятью, которая не может эффективно работать в условиях ограниченного объема памяти. Цель состоит в том, чтобы продемонстрировать, что многие считают невозможным, что значимая Java-программа может работать практически без памяти. Примеры процессов
2,2 миллиона CSV-записей в секунду в куче 3 МБ с нулевым значением gc в одном потоке в Java .

Вы узнаете, где существуют основные области отходов в java-приложении, и какие шаблоны можно использовать для их уменьшения. Представлена ​​концепция абстракции с нулевой стоимостью, и многие оптимизации можно автоматизировать во время компиляции посредством генерации кода. Плагин Maven упрощает рабочий процесс разработчика.

Наша цель — не высокая производительность, а побочный продукт максимальной эффективности. В решении используется Fluxtion, который использует часть ресурсов по сравнению с существующими средами обработки событий Java.

Компьютеры и климат

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

На панельной сессии infoq 2019 в Лондоне Мартин Томпсон с энтузиазмом говорил о создании вычислительных систем для энергоэффективности. Он отметил, что контроль отходов является критическим фактором в минимизации потребления энергии. Комментарии Мартина нашли отклик у меня, так как основная философия Fluxtion заключается в устранении ненужного потребления ресурсов. Эта панельная сессия была вдохновением для этой статьи.

Требования к обработке

Требования к примеру обработки:

  • Работать в 3MB кучи с нулевым GC
  • Используйте только стандартные библиотеки Java, без «небезопасных» оптимизаций
  • Прочитать файл CSV, содержащий миллионы строк входных данных
  • Ввод представляет собой набор неизвестных событий, без предварительной загрузки данных
  • Строки данных являются гетерогенными типами
  • Обработать каждую строку для расчета нескольких совокупных значений
  • Расчеты зависят от типа строки и содержания данных.
  • Применение правил к агрегатам и подсчет нарушений правил
  • Данные распределяются случайным образом для предотвращения прогнозирования ветвлений.
  • Расчет разделов на основе входных значений строк
  • Сбор и группировка разделенных вычислений в сводное представление
  • Опубликовать сводный отчет в конце файла
  • Чисто Java решение с использованием функций высокого уровня
  • Нет разогрева JIT

Пример позиции и мониторинга прибыли

Файл CSV содержит сделки и цены для ряда активов, по одной записи на строку. Расчеты позиции и прибыли для каждого актива разделены в их собственном пространстве памяти. Расчеты активов обновляются при каждом соответствующем входном событии. Прибыль по всем активам будет агрегирована в портфельную прибыль. Каждый актив отслеживает свою текущую позицию / состояние прибыли и записывает счет, если любой из них нарушает установленный лимит. Прибыль портфеля будет отслеживаться, а убытки учитываться.

Правила проверяются на уровне активов и портфеля для каждого входящего события. Количество нарушений правил обновляется по мере поступления событий в систему.

Типы данных строк

1
href="https://github.com/gregv12/articles/blob/article_may2019/2019/may/trading-monitor/src/main/java/com/fluxtion/examples/tradingmonitor/AssetPrice.java" target="_blank" rel="noopener noreferrer">AssetPrice - [price: double] [symbol: CharSequence]<br><br><a href="https://github.com/gregv12/articles/blob/article_may2019/2019/may/trading-monitor/src/main/java/com/fluxtion/examples/tradingmonitor/Deal.java" target="_blank" rel="noopener noreferrer">Deal</a>       - [price: double] [symbol: CharSequence] [size: int]

Образец данных

CSV-файл содержит строки заголовка для каждого типа, чтобы обеспечить динамическое расположение столбцов для сопоставления полей. Каждой строке предшествует простое имя класса целевого типа, в который нужно выполнить маршалирование. Примерный набор записей, включая заголовок:

1
2
3
4
5
Deal,symbol,size,price
AssetPrice,symbol,price
AssetPrice,FORD,15.0284
AssetPrice,APPL,16.4255
Deal,AMZN,-2000,15.9354

Описание расчета

Расчеты активов разделены по символам и затем собраны в расчет портфеля.

Расчеты по разделенным активам

1
2
3
4
5
asset position  = sum(Deal::size)
deal cash value = (Deal::price) X (Deal::size) X -1
cash position   = sum(deal cash value)
mark to market  = (asset position) X (AssetPrice::price)
profit          = (asset mark to market) + (cash position)

Портфельные расчеты

1
portfolio profit = sum(asset profit)

Правила мониторинга

1
2
3
asset loss > 2,000
asset position outside of range +- 200
portfolio loss > 10,000

НОТА:

  1. Подсчет производится, когда уведомитель указывает на нарушение правила. Уведомитель срабатывает только при первом нарушении, пока оно не будет сброшено. Уведомитель сбрасывается, когда правило снова становится действительным.
  2. Положительная сделка :: размер — покупка, отрицательная — продажа.

Среда исполнения

Для обеспечения требований к памяти (ноль GC и 3 МБ кучи)
Используется сборщик мусора Epsilon no-op, максимальный размер кучи которого составляет 3 МБ. Если на протяжении всего жизненного цикла процесса выделено более 3 МБ памяти, JVM немедленно завершит работу с ошибкой нехватки памяти.

Для запуска примера: клон из git и в корне проекта trading-monitor запустите файл jar в каталоге dist, чтобы сгенерировать файл тестовых данных из 4 миллионов строк.

1
2
3
git clone --branch  article_may2019 https://github.com/gregv12/articles.git
cd articles/2019/may/trading-monitor/
jdk-12.0.1\bin\java.exe -jar dist\tradingmonitor.jar 4000000

По умолчанию tradingmonitor.jar обрабатывает файл data / generate-data.csv. Используя указанную выше команду, входные данные должны иметь 4 миллиона строк и иметь длину 94 МБ, готовых к выполнению.

Полученные результаты

Чтобы выполнить тест, запустите tradingmonitor.jar без аргументов:

1
jdk-12.0.1\bin\java.exe -verbose:gc -Xmx3M -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar dist\tradingmonitor.jar

Выполнение теста для 4 миллионов строк сводные результаты:

01
02
03
04
05
06
07
08
09
10
11
Process row count     =    4 million
Processing time       =    1.815 seconds
Avg row exec time     =  453 nano seconds
Process rate          =    2.205 million records per second
garbage collections   =    0
allocated mem total   = 2857 KB
allocated mem per run =   90 KB
OS                    = windows 10
Processor             = Inte core [email protected]
Memory                = 16 GB
Disk                  = 512GB Samsung SSD PM961 NVMe

ПРИМЕЧАНИЕ. Результаты первого прогона без прогрева JIT. После прогрева Jit время выполнения кода примерно на 10% быстрее. Общий объем выделенной памяти составляет 2,86 МБ, включая запуск JVM.

Анализируя вывод Epsilon, мы оцениваем, что приложение выделяет 15% памяти на 6 запусков, или 90 КБ на прогон. Существует большая вероятность того, что данные приложения будут помещаться в кэш-память первого уровня, здесь необходимо провести дополнительные исследования.

Выход

Тестовая программа зацикливается 6 раз, выводя результаты каждый раз, Epsilon записывает статистику памяти в конце цикла.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
jdk-12.0.1\bin\java.exe" -server -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC  -Xmx3M -verbose:gc -jar dist\tradingmonitor.jar
[0.011s][info][gc] Non-resizeable heap; start/max: 3M
[0.011s][info][gc] Using TLAB allocation; max: 4096K
[0.011s][info][gc] Elastic TLABs enabled; elasticity: 1.10x
[0.011s][info][gc] Elastic TLABs decay enabled; decay time: 1000ms
[0.011s][info][gc] Using Epsilon
[0.024s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 0M (5.11%) used
[0.029s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 0M (10.43%) used
.....
.....
[0.093s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 1M (64.62%) used
[0.097s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 2M (71.07%) used
 
 
portfolio loss gt 10k count -> 792211.0
Portfolio PnL:-917.6476000005273
Deals processed:400346
Prices processed:3599654
Assett positions:
-----------------------------
[1.849s][info][gc] Heap: 3M reserved, 3M (100.00%) committed, 2M (76.22%) used
MSFT : AssetTradePos{symbol=MSFT, pnl=484.68589999993696, assetPos=97.0, mtm=1697.0247000000002, cashPos=-1212.3388000000632, positionBreaches=139, pnlBreaches=13628, dealsProcessed=57046, pricesProcessed=514418}
GOOG : AssetTradePos{symbol=GOOG, pnl=-998.6065999999155, assetPos=-1123.0, mtm=-19610.1629, cashPos=18611.556300000084, positionBreaches=3, pnlBreaches=105711, dealsProcessed=57199, pricesProcessed=514144}
APPL : AssetTradePos{symbol=APPL, pnl=-21.881300000023202, assetPos=203.0, mtm=3405.1017, cashPos=-3426.9830000000234, positionBreaches=169, pnlBreaches=26249, dealsProcessed=57248, pricesProcessed=514183}
ORCL : AssetTradePos{symbol=ORCL, pnl=-421.9756999999504, assetPos=-252.0, mtm=-4400.4996, cashPos=3978.5239000000497, positionBreaches=103, pnlBreaches=97777, dealsProcessed=57120, pricesProcessed=513517}
FORD : AssetTradePos{symbol=FORD, pnl=112.14559999996254, assetPos=-511.0, mtm=-7797.8089, cashPos=7909.9544999999625, positionBreaches=210, pnlBreaches=88851, dealsProcessed=57177, pricesProcessed=514756}
BTMN : AssetTradePos{symbol=BTMN, pnl=943.8932999996614, assetPos=-1267.0, mtm=-19568.9417, cashPos=20512.83499999966, positionBreaches=33, pnlBreaches=117661, dealsProcessed=57071, pricesProcessed=514291}
AMZN : AssetTradePos{symbol=AMZN, pnl=-557.0849999999355, assetPos=658.0, mtm=10142.214600000001, cashPos=-10699.299599999937, positionBreaches=63, pnlBreaches=114618, dealsProcessed=57485, pricesProcessed=514345}
-----------------------------
Events proecssed:4000000
millis:1814
...
...
portfolio loss gt 10k count -> 792211.0
Portfolio PnL:-917.6476000005273
Deals processed:400346
Prices processed:3599654
Assett positions:
-----------------------------
MSFT : AssetTradePos{symbol=MSFT, pnl=484.68589999993696, assetPos=97.0, mtm=1697.0247000000002, cashPos=-1212.3388000000632, positionBreaches=139, pnlBreaches=13628, dealsProcessed=57046, pricesProcessed=514418}
GOOG : AssetTradePos{symbol=GOOG, pnl=-998.6065999999155, assetPos=-1123.0, mtm=-19610.1629, cashPos=18611.556300000084, positionBreaches=3, pnlBreaches=105711, dealsProcessed=57199, pricesProcessed=514144}
APPL : AssetTradePos{symbol=APPL, pnl=-21.881300000023202, assetPos=203.0, mtm=3405.1017, cashPos=-3426.9830000000234, positionBreaches=169, pnlBreaches=26249, dealsProcessed=57248, pricesProcessed=514183}
ORCL : AssetTradePos{symbol=ORCL, pnl=-421.9756999999504, assetPos=-252.0, mtm=-4400.4996, cashPos=3978.5239000000497, positionBreaches=103, pnlBreaches=97777, dealsProcessed=57120, pricesProcessed=513517}
FORD : AssetTradePos{symbol=FORD, pnl=112.14559999996254, assetPos=-511.0, mtm=-7797.8089, cashPos=7909.9544999999625, positionBreaches=210, pnlBreaches=88851, dealsProcessed=57177, pricesProcessed=514756}
BTMN : AssetTradePos{symbol=BTMN, pnl=943.8932999996614, assetPos=-1267.0, mtm=-19568.9417, cashPos=20512.83499999966, positionBreaches=33, pnlBreaches=117661, dealsProcessed=57071, pricesProcessed=514291}
AMZN : AssetTradePos{symbol=AMZN, pnl=-557.0849999999355, assetPos=658.0, mtm=10142.214600000001, cashPos=-10699.299599999937, positionBreaches=63, pnlBreaches=114618, dealsProcessed=57485, pricesProcessed=514345}
-----------------------------
Events proecssed:4000000
millis:1513
[14.870s][info][gc] Total allocated: 2830 KB
[14.871s][info][gc] Average allocation rate: 19030 KB/sec

Отработанные горячие точки

В приведенной ниже таблице указаны функции в цикле обработки, которые традиционно создают методы предотвращения отходов и отходов, используемые в примере.

функция Источник отходов эффект уклонение
Читать файл CSV Выделите новую строку для каждой строки GC Прочитайте каждый байт во взвешенном состоянии и обработайте в выделенном свободном декодере
Держатель данных для строки Выделите экземпляр данных для каждой строки GC Flyweight один экземпляр данных
Читать значения col Выделите массив строк для каждого столбца GC Вставьте символы в многоразовый буфер
Преобразовать значение в тип Преобразование строк в типы выделяет память GC Преобразователи с нулевым распределением CharSequence вместо Strings
Нажмите значение col для держателя Автобокс для примитивных типов выделяет память. GC Примитивные осведомленные функции проталкивают данные. Нулевое распределение
Разделение обработки данных Разделы данных обрабатываются параллельно. Задачи, распределенные по очередям GC / Lock Обработка одного потока, без выделения или блокировки
вычисления Автобокс, неизменяемые типы, выделяющие промежуточные экземпляры. Свободные от состояния функции требуют внешнего хранения состояния и распределения GC Генерация функций без автобокса. Stateful функции нулевого распределения
Сбор сводной калькуляции Перенесите результаты из потоков раздела в очередь. Требует выделения и синхронизации GC / Lock Обработка одного потока, без выделения или блокировки

Решения по сокращению отходов

Код, который реализует обработку событий, генерируется с использованием Fluxtion. Генерация решения допускает подход абстракции с нулевой стоимостью, когда скомпилированное решение имеет минимальные накладные расходы. Программист описывает желаемое поведение, и во время сборки генерируется оптимизированное решение, которое соответствует требованиям. Для этого примера сгенерированный код можно посмотреть здесь .

Maven Pom содержит профиль для восстановления сгенерированных файлов с использованием плагина Fluxtion Maven, выполняемого с помощью следующей команды:

1
mvn -Pfluxtion install

Чтение файлов

Данные извлекаются из входного файла в виде серии CharEvents и публикуются в маршаллере типа CSV. Каждый символ отдельно читается из файла и помещается в CharEvent. Поскольку один и тот же экземпляр CharEvent используется повторно, после инициализации память не выделяется. Логика потоковой передачи CharEvents находится в классе CharStreamer . Весь файл размером 96 МБ можно прочитать с почти нулевой памятью, выделенной приложению в куче.

Обработка CSV

Добавление @CsvMarshaller в javabean уведомляет Fluxtion о создании парсера csv во время сборки. Fluxtion сканирует классы приложений для аннотации @CsvMarshaller и генерирует маршаллеры как часть процесса сборки. Для примера см. AssetPrice.java, который приводит к генерации AssetPriceCsvDecoder0 . Декодер обрабатывает CharEvents и маршаллизирует данные строки в целевой экземпляр.

Сгенерированные парсеры CSV используют стратегии, описанные в таблице выше, избегая ненужного выделения памяти и повторно используя экземпляры объектов для каждой обработанной строки:

  • Один экземпляр многоразового использования буферов символов хранит символы строки
  • Повторно используемый экземпляр в навесном весе является целью для данных маршалловых столбцов
  • Преобразования выполняются непосредственно из CharSequence в целевые типы без создания промежуточного объекта.
  • Если в целевом экземпляре используются CharSequence, то строки не создаются, используется несимметричная последовательность Charsequence.

Для примера бесполезного преобразования символа в целевое поле см. Метод upateTarget () в AssetPriceCsvDecoder:

вычисления

Этот компоновщик описывает расчет активов с использованием потокового API Fluxtion. Декларативная форма похожа на API потока Java, но строит графики обработки событий в реальном времени. Методы, помеченные аннотацией
@SepBuilder вызывается плагином maven для генерации статического обработчика событий. Код ниже описывает расчеты для актива, см.
FluxtionBuilder :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@SepBuilder(name = "SymbolTradeMonitor",
            packageName = "com.fluxtion.examples.tradingmonitor.generated.symbol",
            outputDir = "src/main/java",
            cleanOutputDir = true
    )
    public void buildAssetAnalyser(SEPConfig cfg) {
        //entry points subsrcibe to events
        Wrapper<Deal> deals = select(Deal.class);
        Wrapper<AssetPrice> prices = select(AssetPrice.class);
        //result collector, and republish as an event source
        AssetTradePos results = cfg.addPublicNode(new AssetTradePos(), "assetTradePos");
        eventSource(results);
        //calculate derived values
        Wrapper<Number> cashPosition = deals
                .map(multiply(), Deal::getSize, Deal::getPrice)
                .map(multiply(), -1)
                .map(cumSum());
        Wrapper<Number> pos = deals.map(cumSum(), Deal::getSize);
        Wrapper<Number> mtm = pos.map(multiply(), arg(prices, AssetPrice::getPrice));
        Wrapper<Number> pnl = add(mtm, cashPosition);
        //collect into results
        cashPosition.push(results::setCashPos);
        pos.push(results::setAssetPos);
        mtm.push(results::setMtm);
        pnl.push(results::setPnl);
        deals.map(count()).push(results::setDealsProcessed);
        prices.map(count()).push(results::setPricesProcessed);
        //add some rules - only fires on first breach
        pnl.filter(lt(-200))
                .notifyOnChange(true)
                .map(count())
                .push(results::setPnlBreaches);
        pos.filter(outsideBand(-200, 200))
                .notifyOnChange(true)
                .map(count())
                .push(results::setPositionBreaches);
        //human readable names to nodes in generated code - not required
        deals.id("deals");
        prices.id("prices");
        cashPosition.id("cashPos");
        pos.id("assetPos");
        mtm.id("mtm");
        pnl.id("pnl");
    }

Функциональное описание преобразуется в эффективную императивную форму для исполнения. Сгенерированный обработчик событий SymbolTradeMonitor является точкой входа для событий AssetPrice и Deal. Сгенерированные вспомогательные классы используются обработчиком событий для вычисления агрегатов, вспомогательные классы здесь .

Процессор получает события от разделителя и вызывает вспомогательные функции для извлечения данных и вызова функций вычисления, сохраняя совокупные результаты в узлах. Совокупные значения помещаются в поля экземпляра результатов, AssetTradePos. Промежуточные объекты не создаются, любые примитивные вычисления обрабатываются без автобокса. Узлы вычисления ссылаются на данные из родительских экземпляров, никакие объекты данных не перемещаются по графику во время выполнения. Как только график инициализирован, при обработке события не выделяется память.

Изображение, представляющее график обработки для расчета актива, создается одновременно с кодом, как показано ниже:

Аналогичный набор вычислений описан для портфеля в методе buildPortfolioAnalyser класса FluxtionBuilderbuilder , который генерирует обработчик событий PortfolioTradeMonitor . AssetTradePos публикуется из SymbolTradeMonitor в PortfolioTradeMonitor. Сгенерированные файлы для расчетов портфеля находятся здесь .

Разделение и сбор

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

Системный поток данных

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

Вывод

В этой статье я показал, что можно решить сложную проблему обработки событий в Java практически без потерь. Функции высокого уровня использовались в декларативном / функциональном подходе для описания желаемого поведения, и сгенерированные обработчики событий отвечают требованиям описания. Простая аннотация запускает маршаллерское поколение. Сгенерированный код — это простой императивный код, который JIT может легко оптимизировать. Не выполняется ненужное выделение памяти, и экземпляры используются повторно в максимально возможной степени.

Следуя этому подходу, высокопроизводительные решения с низким потреблением ресурсов доступны для среднего программиста. Традиционно только специальные инженеры с многолетним опытом могли достичь этих результатов.

Хотя этот подход является новым в Java, он знаком в других языках, широко известных как абстракция с нулевой стоимостью.

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

Опубликовано на Java Code Geeks с разрешения Грега Хиггинса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: безотходное кодирование

Мнения, высказанные участниками Java Code Geeks, являются их собственными.