Русский
Русский
English
Статистика
Реклама

Бенчмаркинг

Перевод Асинхронный Python-код медленнее обычного кода

22.06.2020 16:10:43 | Автор: admin
Большинство программистов понимают то, что асинхронный Python-код имеет более высокий уровень конкурентности, чем обычный синхронный код. Это даёт некоторые основания полагать, что асинхронный код способен показывать более высокий уровень производительности при решении распространённых задач вроде выдачи динамических веб-страниц или поддержки веб-API.



Но, к сожалению, Python-интерпретатор не выполняет асинхронный код быстрее синхронного.

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

Результаты бенчмарка


Я протестировал широкий набор различных синхронных и асинхронных конфигураций веб-серверов. В следующей таблице показаны результаты испытаний.
Веб-сервер Фреймворк Воркеры Задержки, P50, мс Задержки, P99, мс Пропускная способность,
запросов в секунду
Gunicorn с meinheld Falcon 16 17 31 5589
Gunicorn с meinheld Bottle 16 17 32 5780
UWGGI через http Bottle 16 18 32 5497
UWSGI через http Flask 16 22 33 4431
Gunicorn с meinheld Flask 16 21 35 4510
UWSGI через 'uwsgi' Bottle 16 18 36 5281
UWSGI через http Falcon 16 18 37 5415
Gunicorn Flask 14 28 42 3473
Uvicorn Starlette 5 16 75 4952
AIOHTTP AIOHTTP 5 19 76 4501
Uvicorn Sanic 5 17 85 4687
Gunicorn с gevent Flask 12 24 136 3077
Daphne Starlette 5 20 364 2678

50 и 99 перцентили времени отклика измерены в миллисекундах. Пропускная способность измерена в запросах в секунду. Данные в таблице отсортированы по 99 перцентилю. Я полагаю, что, если речь идёт о реальном применении технологий, это самый важный показатель.

Обратите внимание на следующее:

  1. Лучше всего показывают себя синхронные фреймворки, однако Flask даёт худшие результаты, чем другие фреймворки.
  2. Худшие результаты имеют все асинхронные фреймворки.
  3. Для асинхронных фреймворков характерна очень сильная изменчивость задержек.
  4. Конфигурации, в которых используется uvloop, альтернативная реализации цикла событий для asyncio, показывают более высокие результаты, чем asyncio-конфигурации без uvloop. Это значит, что если вы вынуждены пользоваться asyncio-решениями, вам стоит применять и uvloop.

Репрезентативны ли результаты этого бенчмарка?


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


Архитектура тестовой среды: генератор нагрузки, обратный прокси-сервер, веб-сервер, менеджер соединений, база данных

Я попытался сделать всё возможное для того чтобы как можно точнее смоделировать то, что используется в реальном мире. Здесь имеется обратный прокси (nginx), код, написанный на Python, и база данных (PostgreSQL). Я, кроме того, использовал здесь менеджер соединений для PostgreSQL (PgBouncer), так как полагаю, что в реальных конфигурациях, по крайней мере, там, где используется PostgreSQL, этот менеджер встречается достаточно часто.

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

Почему в разных конфигурациях используется разное количество воркеров?


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

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

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

Подробнее об этом можно почитать в документации к gunicorn:

Обычно мы рекомендуем, для начала, устанавливать количество воркеров, пользуясь следующей формулой: (2 x $num_cores) + 1. Эта формула построена исходя из предположения о том, что один воркер, выполняющийся на некоем процессорном ядре, будет заниматься чтением данных из сокета или записью данных в сокет, а в это время другой воркер будет заниматься обработкой запроса. За этой формулой не стоят некие глубокие научные исследования.

Характеристики сервера


Я запускал бенчмарк на сервере, соответствующем тарифному плану Hetzner CX31. Речь идёт о 4 vCPU и о 8 Гб памяти. Сервер работал под управлением Ubuntu 20.04. Генератор нагрузки был запущен на другой виртуальной машине (менее мощной).

Почему асинхронный код работает хуже синхронного?


Пропускная способность


Если говорить о пропускной способности системы (то есть о том, сколько запросов она способна обработать в секунду), то главным фактором здесь является не то, синхронный или асинхронный код в ней используется, а то, сколько Python-кода в ней было заменено на нативный код. Проще говоря: чем больше Python-кода, влияющего на производительность, можно заменить нативным кодом, тем лучше. Это подход к производительности в Python, у которого долгая история (взгляните, например, на numpy).

Meinheld и UWSGI (~5300 запросов в секунду) содержат большие объёмы кода, написанного на C. Стандартный Gunicorn (~3400 запросов в секунду) это чистый Python.

В связке Uvicorn+Starlette (~4900 запросов в секунду) на более производительный код заменено гораздо больше Python-кода, чем в стандартном сервере AIOHTTP (~4500 запросов в секунду). Правда, тут надо отметить, что AIOHTTP был установлен с необязательными ускорителями.

Задержки


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

Почему это так? Дело в том, что в асинхронном Python-коде реализована кооперативная многопоточность. Это, если объяснить по-простому, означает, что выполнение потоков не прерывается неким менеджером (например планировщиком ядра). Потоки, вместо этого, отдают ресурсы другим потокам по собственной инициативе. В asyncio для этого используются три команды: await, async for и async with.

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

Традиционные синхронные Python-серверы, вроде UWSGI, используют режим с принудительным распределением ресурсов между процессами. За это отвечает планировщик ядра. Это позволяет обеспечить справедливое распределение ресурсов путём периодического переключения процессов. Это означает, что процессорное время распределяется между процессами более справедливо, что ведёт к меньшей изменчивости величины задержек.

Почему другие бенчмарки дают другие результаты?


Основная масса других бенчмарков (особенно тех, которые написаны авторами асинхронных фреймворков!) просто не исследует синхронные фреймворки с достаточным количеством воркеров. Это означает, что у исследуемых синхронных фреймворков просто нет возможности задействовать процессоры на полную мощность.

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


Тестирование фреймворка Vibora

Результаты говорят о том, что пропускная способность Vibora на 500% выше, чем пропускная способность Flask. Правда, когда я почитал код использованного здесь бенчмарка, оказалось, что Flask тут был настроен далеко не самым оптимальным образом: на одно ядро приходился всего один воркер. Когда я это исправил, я получил то, что показано в следующей таблице.
Фреймворк Пропускная способность, запросов в секунду
Flask 11925
Vibora 14148

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

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

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

Дальнейшие рассуждения, предположения и результаты наблюдений


Хотя мой бенчмарк и достаточно реалистичен, если рассматривать его через призму используемых в нём технологий, нагрузка, которой я подвергаю сервер, гораздо однороднее той, которой подвергаются реальные серверы. А именно, в моём бенчмарке все запросы вызывают обращения к базе данных, при этом с тем, что пришло из базы данных, всегда делается одно и то же. В реальных приложениях всё, как правило, выглядит иначе. Например, в них могут встречаться быстрые и медленные операции. Некоторые из операций предусматривают интенсивное использование подсистемы ввода-вывода, а некоторые сильно нагружают процессор. Правильным мне кажется исходить из предположения о том (и опыт подсказывает мне, что так оно и есть), что в реальных приложениях диапазон изменения значений задержек будет гораздо больше.

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

Так, Дэн Мак-Кинли писал о своём опыте работы с системой, основанной на фреймворке Twisted и используемой в Etsy. Похоже, что эта система страдала от хронической изменчивости задержек:

[Консультант по Twisted] сказал, что, хотя Twisted отличается высоким уровнем общей пропускной способности, некоторые запросы могут сталкиваться с очень сильными задержками. А это [для систем Etsy] было проблемой, потому что PHP-фронтенд обращался к Twisted сотни или тысячи раз на один веб-запрос.

Майк Байер, автор SQLAlchemy, несколько лет назад написал материал об асинхронном Python-коде и базах данных. Там он рассматривал асинхронный код с несколько иной точки зрения. Он тоже делал бенчмарки и выяснил, что asyncio отличается меньшей эффективностью, чем можно было бы ожидать.

На ресурсе rachelbythebay.com опубликована статья, в которой речь идёт о беспорядке, возникающем от использования конфигураций, основанных на gevent. Хочу отметить, что и я сталкивался с проблемами (хотя и не относящимися к производительности), когда использовал gevent в продакшне.

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

Например, uvicorn останавливал родительский процесс, не останавливая дочерних процессов. А это означало, что мне нужно было охотиться за этими процессами, которые всё ещё держались за порт 8001. Однажды AIOHTTP выдал внутреннюю критическую ошибку, которая касалась файловых дескрипторов, но не остановился. А это значит, что его не смог бы перезапустить какой-нибудь менеджер процессов. Это, на мой взгляд, прямо-таки смертный грех. Проблемы были и у Daphne, но я уже не помню точно, что именно случилось.

Все эти ошибки случались нечасто, их легко можно было исправить с помощью SIGKILL. Но факт остаётся фактом: не хотелось бы мне столкнуться с чем-то таким в продакшне. В то же время, у меня, например, не было никаких проблем с Gunicorn и UWSGI, за исключением того, что мне сильно не понравилось то, что UWSGI не завершает работу в том случае, если приложение не было правильно загружено.

Итоги


В итоге могу порекомендовать всем тем, кому нужна высокая производительность Python-кода, просто использовать обычные синхронные решения. При этом стоит заменить как можно большие объёмы Python на что-то более быстрое. В случае с веб-серверами, если важна пропускная способность системы, стоит обратить внимание на все фреймворки за исключением Flask. Правда, даже Flask в паре с UWSGI отличается показателями задержек, которые ставят его в один ряд с самыми лучшими фреймворками.

Пользуетесь ли вы серверными решениями, написанными на Python?



Подробнее..

Если ты видишь статью, что язык Х быстрее, чем язык Y можешь закрывать статью

28.07.2020 20:05:54 | Автор: admin


Я своими гуманитарными мозгами всегда думал так если программист знает, как сделать перфоманснее значит надо сделать перфоманснее. Производительное решение = правильное решение. Один язык программирования может быть медленнее другого, и если это выяснится язык программирования отправляется на помойку.

Ну и уж точно если разработчик специалист в области перфоманса, он будет топить за все эти вещи, даже если они неверны.

Естественно, все это чушь, но не мне вам об этом говорить. Поэтому к нам в подкаст пришел Андрей Акиньшин разработчик и математик, кандидат физико-математических наук, мейнтейнер BenchmarkDotNet и perfolizer, автор книги Pro .NET Benchmarking и просто очень, очень крутой инженер.


Ниже избранные цитаты.

Предусмотреть в бенчмарках все невозможно


У моего коллеги недавно случилось следующее. Он программировал с утра, у него всё было хорошо, всё работало быстро. В какой-то момент всё начало втыкать Rider работает медленно, IDEA, браузер всё работает медленно. Он никак не мог понять, в чём же дело? А потом понял. Он работал на ноутбуке чёрного цвета, который стоял у окна. С утра было довольно прохладно, а днём поднялось солнышко, ноутбук очень сильно нагрелся и ушёл в термальный троттлинг.

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

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

Я приведу аналогию с языками. Когда ты учишь свой первый функциональный ЯП, тебе нужно немного модифицировать своё отношение к миру понять принципы функционального программирования, то, как вообще нужно мыслить. Потом ты берёшь следующий функциональный язык Х, и у тебя в голове эти принципы уже есть. Ты смотришь пару hello world и тоже начинаешь писать.

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

Я считаю, что нужно в каждой области доходить до похожего уровня, а дальше идти вширь.

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

Бенчмаркинг ради бенчмаркинга не самое лучшее, чем можно заниматься


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

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

Грубо говоря, если у меня в коллекции есть максимум 10 элементов, и есть два варианта написать простой алгоритм за куб или очень сложный за n*log n я напишу простой за куб, который будет понятен всем, который будет легко поддерживать и модифицировать. Потому что я понимаю мои перфомансные ограничения он никогда в жизни не пробьёт.

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

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

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

Пока этой интуиции нет, можно идти методом проб и ошибок и смотреть, что получается.

У тебя есть всегда трейдоф между перфомансом и красотой


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

Я считаю, нужно ориентироваться на текущие бизнес-требования, и в рамках них писать максимально чистый, понятный, красивый, поддерживаемый код. А в тот момент, когда оно начинает жать, (или есть ощущение, что скоро начнёт), то тогда уже что-то менять.

И даже если ты будешь всегда концентрироваться исключительно на перфомансе нет такой штуки, как идеально оптимизированный, максимально производительный код. Это значит всё забыли про C#, забыли про все красивые языки. А лучше вообще писать в машинных кодах, потому что Ассемблер он тоже по синтаксису ограничен. А если сразу по байтикам писать, то ты получишь перфоманс-boost.

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

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

У языка нет такого свойства как перфоманс


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

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

Например, в условном языке на ранних стадиях развития реализовали метод для сортировки с помощью пузырька. Ну, не знаю ребята хотели побыстрее выкатить релиз, написали простейшую сортировку, которую могли сделать. Ты взял, заиспользовал этот метод, и он оказался медленнее на больших данных, чем в другом языке, где сделан quicksort. Значит ли это, что ты можешь говорить о перфомансе каких-то языков? Нет. Ты можешь говорить, что вот эта конкретно апишка из этого языка на этой операционной системе, вот на этом железе, вот в этих окружениях работает медленнее, чем другая апишка из другого языка в другом environment. Так можно сказать. Но получится очень длинный абзац текста, чтобы сформулировать правильно.

Условно можно сказать, что C++ в большинстве случаев работает быстрее, чем JavaScript. Но более правильно будет сказать, что С++ программисты с хорошим опытом С++, которые пишут на С++, напишут программу, которая скорее всего будет быстрее, чем джаваскриптер на JavaScript напишет что-то, что будет работать в браузере.

Но и здесь есть очень много оговорок. А что если чувак, который писал на JavaScript скажет, что это не так, и пойдёт там на какой-нибудь там WebAssembly или еще чего-нибудь переделывать. Или найдет на GitHub суперинтерпретатор-компилятор JavaScript, который работает с очень усечённым подмножеством JS на три с половиной синтаксические конструкции, но зато выдаёт сверхбыстрый нативный код.

И вот там при желании можно написать такой код, который обгонит С++. Более того, можно написать свой компилятор JavaScript, который будет задизайнен, чтобы компилировать одну-единственную программу и обогнать по скорости плюсы. И Это, в принципе валидный вариант.

Социальное давление популярного опенсорсного проекта


С ростом и известностью проектов приходит некий уровень ответственности. Но на самом деле у тебя не появляются обязательства. Этот факт не всегда просто понять, особенно когда приходят всякие люди на GitHub и говорят: У меня вот здесь не работает! Почините срочно! Мне очень надо, чтоб вот это заработало. Идите и чините! Или чувак заводит ишью, а я в отпуске. Проходит три-четыре дня, я даже не видел того, что он что-то там завёл. Я где-то отдыхаю, и чувак начинает Какого хрена вы мне не отвечаете? Что вообще у этого проекта за комьюнити?! Вы вообще все отвратительные люди, с вами надо делать плохие вещи! Я потратил своё время, написал вам, что вы неправы, а вы с этим вообще ничего не делаете, уже четыре дня меня игнорируете! Как так можно?!

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

И вот, когда появляется иммунитет против людей, которые от тебя что-то хотят, то жить становится намного проще. Сейчас я добираюсь до BenchmarkDorNet, когда у меня есть время и настроение покодить. Я знаю, что там много багов. Они в основном некритичные, и касаются каких-то маргинальных случаев в таком-то окружении с последней превьюхой пятого DotNet что-то где-то не работает. Ну и ладно, пущай не работает. Когда у меня будет настроение, я пойду и пофикшу.

Если другим людям нужно, они могут пофиксить сами и прислать пулреквест сделаю ревью, когда у меня будет время и настроение.



Смотрите весь подкаст здесь.
Подробнее..

Опыт команды PVS-Studio повышение производительности C анализатора на Windows при переходе на Clang

31.05.2021 18:12:13 | Автор: admin

С самого своего начала C++ анализатор PVS-Studio для Windows (тогда еще Viva64 версии 1.00 в 2006 году) собирался компилятором MSVC. С выходом новых релизов C++ ядро анализатора научилось работать на Linux и macOS, и структура проекта была переведена на использование CMake. Но под Windows сборка по-прежнему происходила с помощью компилятора MSVC. 29 апреля 2019 года разработчики Visual Studio объявили о включении в свою среду разработки набора утилит LLVM и компилятора Clang. И сейчас у нас наконец дошли руки, чтобы попробовать его в действии.

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

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

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

После того как сборка C++ ядра перешла на Clang, SelfTester стал проходить на 11 минут быстрее.

Выигрыш по производительности в 13% это довольно заметно, учитывая, что достаточно просто поменять компилятор, не так ли?

Минусы тоже есть, но незначительные. Сборка дистрибутива замедлилась на 8 минут, а размер исполняемого файла подрос на 1,6 Мбайт (из них ~500 Кбайт из-за статической линковки рантайма).

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

Далее хочется поделиться подводными камнями, возникшими в процессе перехода.

Генерация сборки под Clang

Скрипты CMake позволяют нам собирать свой код всеми мейнстримными компиляторами под нужные операционные системы.

Прежде всего необходимо установить компоненты компилятора Clang через Visual Studio Installer.

Clang-cl это так называемый "драйвер", который позволяет использовать clang с параметрами от cl.exe. Таким образом, он должен прозрачно взаимодействовать с MSBuild, практически как родной компилятор.

Также можно воспользоваться официальными сборками от проекта LLVM, которые можно найти на их репозитории GitHub. Однако для них нужно установить дополнительный плагин, чтобы Visual Studio смогла найти компиляторы. Имя toolset'а будет llvm, а не clangcl, как показано дальше в примерах.

Указываем toolchain в команде генерации solution для Visual Studio:

cmake -G "Visual Studio 16 2019" -Tclangcl <src>

Либо используем GUI:

Открываем получившийся проект, собираем. И, конечно же, получаем пачку ошибок.

Чиним сборку

Хоть сlang-cl внешне и ведет себя как CL, под капотом это совсем другой компилятор, со своими приколами.

Мы стараемся не игнорировать предупреждения компиляторов, поэтому используем флаги /W4 и /WX. Однако Clang может генерировать дополнительные предупреждения, которые сейчас мешают сборке. Пока выключим их:

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")  ....  if (WIN32)    add_compile_options(-Wno-error=deprecated-declarations                        -Wno-error=reorder-ctor                        -Wno-error=format-security                        -Wno-error=macro-redefined                        -Wno-error=bitwise-op-parentheses                        -Wno-error=missing-field-initializers                        -Wno-error=overloaded-virtual                        -Wno-error=invalid-source-encoding                        -Wno-error=multichar                        -Wno-unused-local-typedef                        -Wno-c++11-narrowing)  ....  endif()endif()

Немного получше.

Компиляторы GCC и Clang имеют встроенную поддержку типа int128, в отличие от MSVC под Windows. Поэтому в своё время была написана обертка с реализацией Int128 для Windows (на ассемблерных вставках и обернутая ifdef'ами, в лучших традициях C/C++). Поправим определения для препроцессора, заменив:

if (MSVC)  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

на

if (MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

Обычно библиотеку с builtin'ами линкеру (lld) передает драйвер компилятора, будь то clang.exe или clang-cl.exe. Но в данном случае линкером заправляет MSBuild напрямую, который не знает, что нужно её использовать. Соответственно, драйвер никак не может передать флаги линкеру, поэтому приходится разбираться самим.

if (CMAKE_GENERATOR MATCHES "Visual Studio")  link_libraries("$(LLVMInstallDir)\\lib\\clang\\\${CMAKE_CXX_COMPILER_VERSION}\\lib\\windows\\\clang_rt.builtins-x86_64.lib")else()  link_libraries(clang_rt.builtins-x86_64)endif()

Ура! Сборка заработала. Однако дальше при запуске тестов нас ждала куча ошибок сегментации:

Отладчик при этом показывает какое-то странное значение в IntegerInterval, а на самом деле проблема находится немного дальше:

В разных структурах для Dataflow-механизма активно применяется ранее упомянутый тип Int128, а для работы с ним используются SIMD-инструкции. И падение вызвано невыровненным адресом:

Инструкция MOVAPS перемещает из памяти набор чисел с плавающей запятой в регистры для SIMD-операций. Адрес при этом обязан быть выровнен, в его конце должен стоять 0, а оказалась 8. Придется помочь компилятору, задав правильное выравнивание:

class alignas(16) Int128

Порядок.

Последняя проблема вылезла из-за Docker-контейнеров:

Сборка под MSVC всегда делалась со статической линковкой рантайма, а для экспериментов с Clang рантайм переключили на динамический. Оказалось, что в образах с Windows по умолчанию не установлены Microsoft Visual C++ Redistributable. Решили вернуть статическую линковку, чтобы у пользователей не возникало таких же неприятностей.

Заключение

Несмотря на то, что пришлось немного повозиться с подготовкой проекта, мы остались довольны ростом производительности анализатора более чем на 10%.

Последующие релизы PVS-Studio на Windows будут собираться с помощью компилятора Clang.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov, Sergey Larin. PVS-Studio Team: Switching to Clang Improved PVS-Studio C++ Analyzer's Performance.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru