Строка кода, вызывающая потерю производительности на 13%.

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

Вступление

Одна из целей нашего проекта - обнаружение критических для производительности фрагментов кода. Чтобы найти строки или фрагменты кода, которые негативно влияют на производительность, мы должны выполнить и профилировать их. Мы делаем это при выполнении тестов на Hyrise. Обычно используемый и стандартизированный тест - TPC-H.

Фактически это тест поддержки принятия решений, состоящий из набора SQL-запросов для анализа данных. Запросы выполняются в сгенерированных таблицах, имитирующих бизнес-ориентированные данные. Наша исследовательская база данных уже поддерживает тест TPC-H, но не в полной мере. Поскольку Hyrise все еще находится в стадии разработки, он еще не поддерживает все типы запросов и типы данных. Следовательно, мы можем выполнить только 8 из 22 запросов с небольшими изменениями. Тем не менее, тест хорошо подходит для поиска потенциальных узких мест и повышения общей производительности базы данных.

Поиск первого узкого места

Прежде всего, мы запустили тесты с включенным инструментом Valgrind Callgrind (см. Последний пост в блоге). Для каждого запроса мы создали столбчатую диаграмму с накоплением, которая отображает относительное время, необходимое для каждого оператора (см. Рисунок 1). Поскольку Callgrind использует метод инструментального профилирования, в код добавляются дополнительные инструкции. Таким образом, время выполнения значительно увеличивается. Поскольку нас интересовало только приблизительное время, мы выбрали относительно небольшой коэффициент масштабирования для создаваемых таблиц. При этом важно помнить, что это может привести к неточным результатам: когда таблица растет линейно, время выполнения оператора часто увеличивается сверхлинейно (например, объединяется).

Поэтому мы также проанализировали производительность с Apple Instruments и более высоким коэффициентом масштабирования. Кроме того, мы оценили измерения времени, предоставленные самой компанией Hyrise. База данных способна распечатать физический план запроса, включая время выполнения для каждого оператора.

В документе Анализ TPC-H: скрытые сообщения и уроки, извлеченные из влиятельного эталонного теста »содержится анализ узких мест. Он показывает область фокусировки производительности для каждого запроса TPC-H. Мы можем сравнить это с результатами профилирования Hyrise. Это позволяет нам находить в Hyrise операторы, которые вызывают большое время выполнения.

Вначале мы уделили особое внимание запросу TPC-H 1. Мы заметили, что много времени занимает оператор проекции. Однако ожидается, что некоторые операторы, такие как объединения и агрегаты, будут иметь довольно длительное время выполнения, тогда как прогнозы, как правило, будут быстрее. В связи с этим мы начали копать глубже с Callgrind.

С помощью графа вызовов, сгенерированного KCachegrind (см. Рис. 3), мы довольно скоро обнаружили нарушителя производительности. Почти 34,5% времени, затрачиваемого на этот компонент, уходит на метод get_column ().

Что делает этот метод? Давайте рассмотрим его подробнее (см. Фрагмент кода ниже). Контрольный запрос содержит множество операций с данными, хранящимися в разных таблицах. Операторы базы данных обращаются к определенным столбцам из этих таблиц во время выполнения. Метод get_column () помогает читать фактические данные в столбце. Оператор может передать идентификатор столбца вышеупомянутому методу. Это вернет ссылку на столбец таблицы. Он будет использован для дальнейших операций. Как только мы перебираем значения этого временного результата, среди прочего вызывается метод get_column ().

Почему это влияет на производительность?

Метод get_column () является частью так называемого «горячего цикла». Это та часть, где база данных получает доступ к фактическим данным или выполняет над ними операции, такие как сравнения. Эти циклы обычно выполняются повторно. В этом примере мы вызываем метод get_column () каждый раз, когда получаем доступ к одному значению. Поскольку значения в нижележащем столбце не меняются во время выполнения, мы можем безопасно сохранить ссылку на столбец в переменной. Предположим, в нашем столбце есть миллион записей, к которым обращается база данных. Вышеупомянутая функция будет вызываться миллион раз. В этом случае будет достаточно запустить его один раз.

Как будто этого было недостаточно, в строке 2 метод выполняет атомарную загрузку. Таким образом, создается блокировка, чтобы другие потоки не могли получить доступ к переменной. Это очень трудоемкая часть, на которую уходит 14,6% времени выполнения метода (см. Рисунок 3). Если Hyrise работает с несколькими потоками, что определенно происходит в производственной среде, каждый отдельный поток должен ждать, чтобы получить блокировку. Доступ к переменной становится чем-то вроде последовательного процесса.

Мы действительно хотим ждать?

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

Hyrise теперь быстрее?

Как и ожидалось, да. Мы увеличили производительность запроса 1 TPC-H на 13%. Есть и другие хорошие новости: от этого изменения выигрывает не только этот конкретный запрос, но и другие запросы TPC-H. Время выполнения уменьшилось как минимум на 2% (запрос 13 TPC-H) до 14% (запрос 7 TPC-H). Если мы снова посмотрим на оператор проекции через kcachegrind, метод get_column () больше не будет присутствовать в графе вызовов, поскольку время, затраченное на этот метод, незначительно.

Будьте на связи!

- Адриан, Марсель и Тони