Удав укрощает Graal VM

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!


В мире Java за последнее время произошло много интересных событий. Одним из таких событий стал выход первой production ready версии Graal VM.


Лично у меня Graal давно вызывает нескрываемый интерес и я пристально слежу за докладами и последними новостями в этой области. Одно время попался на глаза доклад Криса Талингера. В нём Крис рассказывает как в Twitter удалось получить значительный выигрыш в производительности, применив для настройки Graal алгоритмы машинного обучения. У меня появилось стойкое желание попробовать подобное самому. В этой статье хочу поделится тем, что в итоге получилось.  


Эксперимент


Для реализации эксперимента мне понадобились:


  1. cвежий Graal VM Community Edition. На момент написания статьи это 20.2.0
  2. выделенное облачное окружение для нагрузочного тестирования
  3. NewRelic для сбора метрик
  4. генератор тестовой нагрузки
  5. программа на Python и набор скриптов, для реализации самого алгоритма ML

Если описать задачу сухим языком математики, то она будет выглядеть так


Найти такие значения параметров $inline$A = (a_1,a_2,..,a_n)$inline$ при которых функция
$inline$f(x_1,x_2,..,x_n)$inline$ принимает максимальное значение.


Я решил минимизировать потребление процессорного времени и выбрал такую целевую функцию:


$$display$$f=1/mean(CPUUtilization)$$display$$


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


-Dgraal.MaximumInliningSize -Dgraal.TrivialInliningSize  -Dgraal.SmallCompiledLowLevelGraphSize

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


С постановкой задачи оптимизации разобрались. Теперь пришло время пройтись по шагам
алгоритма оптимизации:


  1. алгоритм делает предположение о том какие параметры оптимальные
  2. меняет конфигурацию JVM и запускает нагрузочный тест
  3. снимает метрики и вычисляет значение целевой функции
  4. делает новое предположение на основе значения целевой функции

Процесс повторяется несколько раз.



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


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


Получение метрик из NewRelic


Для работы с NewRelic REST API необходимо узнать свои APP_ID и API_KEY.
APP_ID — это уникальный идентификатор приложения в системе. Его можно найти в разделе APM.
API_KEY необходимо создать или узнать из настроек профиля в NewRelic.


Структура ответа для всех метрик приблизительно одинакова и имеет следующий вид:


{
  "metric_data": {
    "from": "time",
    "to": "time",
    "metrics_not_found": "string",
    "metrics_found": "string",
    "metrics": [
      {
        "name": "string",
        "timeslices": [
          {
            "from": "time",
            "to": "time",
            "values": "hash"
          }
        ]
      }
    ]
  }
}

В итоге метод для получения метрик будет таким:


def request_metrics(params):
    app_id = "APP_ID"
    url = "https://api.newrelic.com/v2/applications/"+ app_id + "/metrics/data.json"
    headers = {'X-Api-Key':"API_KEY"}
    response = requests.get(url, headers=headers, params=params)
    return response.json()

Для получения CPU Utilzation значение params следующее:


params = {
    'names[]': "CPU/User/Utilization",
    'values[]': "percent",
    'from': timerange[0],
    'to': timerange[1],
    'raw': "false"
}

timerange хранит значения времени начала и конца нагрузочного теста.


Далее парсим запрос и извлекаем необходимые метрики


def get_timeslices(response_json, value_name):
    metrics = response_json['metric_data']['metrics'][0]
    timeslices = metrics['timeslices']
    values = []
    for t in timeslices:
        values.append(t['values'][value_name])
    return values

Алгоритм оптимизации


Перейдем к самому интересному — поиску оптимальных параметров.


Для реализации байесовской оптимизации взял уже готовую библиотеку BayesianOptimization.


Сначала создадим метод для вычисления целевой функции.


def objective_function(maximumInliningSize, trivialInliningSize, smallCompiledLowLevelGraphSize):
    update_config(int(maximumInliningSize), int(trivialInliningSize), int(smallCompiledLowLevelGraphSize))
    timerange = do_test()
    data = get_results(timerange)
    return calculate(data)

Метод _updateconfig вызывает скрипт, который обновляет конфиг приложения. Далее в _dotest происходит вызов скрипта для запуска нагрузочного теста.


Каждое изменение конфигурации требует перезапуска JVM и первые несколько минут идёт фаза прогрева. Эту фазу необходимо отфильтровать, откинув первые минуты.


В методе calculate вычисляем целевую функцию:


    value = 1 / (mean(filtered_data))

Необходимо ограничить поиск


pbounds = {
            'maximumInliningSize': (200, 500),
           'trivialInliningSize': (10, 25),
           'smallCompiledLowLevelGraphSize': (200, 650)
           }

Так как улучшение должно быть относительно настроек по умолчанию, то я добавил соответствующую точку


  optimizer.probe(
        params={"maximumInliningSize": 300.0,
                "trivialInliningSize": 10.0,
                "smallCompiledLowLevelGraphSize": 300.0},
        lazy=True,
        )

Окончательный метод ниже


def autotune():
    pbounds = {
                'maximumInliningSize': (200, 500),
               'trivialInliningSize': (10, 25),
               'smallCompiledLowLevelGraphSize': (200, 650)
               }

    optimizer = BayesianOptimization(
        f=objective_function,
        pbounds=pbounds,
        random_state=1,
    )

    optimizer.probe(
    params={"maximumInliningSize": 300.0,
            "trivialInliningSize": 10.0,
            "smallCompiledLowLevelGraphSize": 300.0},
    lazy=True,
    )

    optimizer.maximize(
        init_points=2,
        n_iter=10,
    )

    print(optimizer.max)

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


При необходимости все итерации можно вывести вот так:


for i, res in enumerate(optimizer.res):
    print("Iteration {}: \n\t{}".format(i, res))

Код выведет значения целевой функции и параметров для каждой итерации.


Iteration 0:
    {'target': 0.02612330198537095, 'params': {'maximumInliningSize': 300.0, 'smallCompiledLowLevelGraphSize': 300.0, 'trivialInliningSize': 10.0}}
Iteration 1:
    {'target': 0.02666666666666667, 'params': {'maximumInliningSize': 325.1066014107722, 'smallCompiledLowLevelGraphSize': 524.1460220489712, 'trivialInliningSize': 10.001715622260173}}
...

Результаты


Сравнил два прогона нагрузочного теста с настройками по умолчанию и вычисленными в ходе эксперимента.


На графике можно заметить, что CPU Utilization уменьшился для случая Graal



Среднее время отклика также незначительно снизилось:



Пропускная способность ограничена сверху и никак не менялась для обоих прогонов.



Заключение


В итоге удалось получить снижение нагрузки CPU в среднем на 4-5%.


Для нашего проекта такая экономия на CPU не существенна, но для proof of concept
результат достаточно неплохой.


С2 много лет оптимизировали под Java и поэтому соревноваться Graal с С2 пока сложно. Больше выгоды можно получить от связки Graal с другими JVM языками, такими как Scala и Kotlin.

Источник: https://habr.com/ru/post/526484/


Интересные статьи

Интересные статьи

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Есть несколько способов добавить водяной знак в Битрикс. Рассмотрим два способа.
Если в вашей компании хотя бы два сотрудника, отвечающих за работу со сделками в Битрикс24, рано или поздно возникает вопрос распределения лидов между ними.
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...
Практически все коммерческие интернет-ресурсы создаются на уникальных платформах соответствующего типа. Среди них наибольшее распространение получил Битрикс24.