Так получилось, что в различных обсуждениях мы уже несколько раз комментировали, как C и C++ модуль PVS-Studio работает с памятью.
А раз так, то пришло время формализовать этот ответ в виде небольшой статьи.
На момент публикации анализатор PVS-Студия содержит три консольных модуля, анализирующих программный код на следующих языках:
- C++, а также в языке C и ряде его диалектов, таких как C++/CLI, C++/CX;
- С#;
- Джава.
Итак, ядро C#-анализатора написано на C#, а ядро Java-анализатора — на Java. В этих языках память освобождается сборщиком мусора, поэтому здесь всё понятно.
Конечно, есть разные вопросы, связанные с оптимизацией.
Например, в статьях [ 1 , 2 , 3 ] коллеги рассматривают возможность уменьшения количества создаваемых временных объектов, настройки сборщика мусора, интернирования и т. д. Но самое интересное, как обстоят дела в ядре C и C++, написанном на C++?
Общий принцип работы ядра
Чтобы было понятно, почему была выбрана та или иная стратегия работы с памятью, следует сначала немного поговорить об общих принципах работы анализатора.Очень важно, что анализ проекта выполняется не целиком сразу, а разбит на множество отдельных этапов.
Запускается новый процесс для анализа каждой единицы перевода (файл .
c, .
cpp).
Это упрощает распараллеливание анализа проекта.
Отсутствие распараллеливания внутри процесса означает, что нет необходимости ничего синхронизировать.
Это, в свою очередь, снижает сложность разработки.
Но внутреннее распараллеливание позволит быстрее сканировать файл? Да, но практического смысла в этом нет. Во-первых, каждый отдельный файл уже сканируется быстро.
Во-вторых, время анализа файлов не сократится пропорционально количеству созданных потоков.
Это может показаться неожиданным, поэтому я объясню.
Прежде чем файл начнет анализироваться, он подвергается предварительной обработке.
Для этого используется внешний препроцессор (компилятор).
Мы никак не контролируем время работы препроцессора.
Грубо предположим, что препроцессор работает 3 секунды, а собственно анализ выполняется еще 3 секунды.
Добавим еще одну условную секунду, которая тратится на сбор информации о файле, запуск процессов, чтение файлов и другие нераспараллеливаемые или плохо распараллеливаемые операции.
Итого 7 секунд. Представим, что будет реализовано внутреннее распараллеливание и сам анализ будет занимать 0,5 секунды, а не 3 секунды.
Тогда общее время сканирования одного файла сократится с привычных 7 секунд до 4,5 секунд. Это приятно, но кардинально ничего не изменилось.
При анализе нескольких файлов такое распараллеливание не имеет смысла, так как будет распараллелен анализ файлов, что, кстати, более эффективно.
А проверка одного-единственного файла, если потребуется, ускорится незначительно.
Но за это небольшое ускорение придется заплатить высокую цену, написав сложный механизм распараллеливания алгоритмов и синхронизации доступа к общим объектам.
Примечание.
Но как PVS-Studio выполняет межмодульный анализ, если каждый процесс обрабатывает только один блок компиляции? Анализ проводится в два прохода.
Сначала необходимая информация собирается и записывается в специальный файл.
Затем файлы повторно анализируются на основе ранее собранной информации [ 4 ].
Стратегия освобождения памяти
Распараллеливание анализа на уровне обработки файлов имеет еще одно важное следствие, касающееся темы использования памяти.Мы не освобождаем память в C++ ядре PVS-Studio до завершения процесса.
Это осознанное, выгодное решение.
А вообще наш единорог всегда просто память съедает :).
Ладно-ладно, это не совсем так.
Локальные объекты, созданные в стеке, естественным образом уничтожаются, а память в куче, которую они выделили для своих нужд, освобождается.
Есть много других объектов, которые живут лишь какое-то время.
Чтобы их своевременно удалить, используются классические умные указатели.
Однако есть три типа данных, которые только создаются, но не уничтожаются до конца процесса:
- Абстрактное синтаксическое дерево;
- Различные данные, собираемые при обходе дерева;
- «Виртуальные значения», используемые для анализа потока данных и символьного выполнения [ 5 ].
Поэтому до тех пор, пока не будет проведена последняя диагностика для последнего узла дерева, все эти данные продолжают храниться.
Перед завершением процесса нет смысла отдельно уничтожать каждый из созданных узлов дерева, а также информацию о том, какие функции могут возвращать и так далее.
Технически, вы можете просмотреть все сохраненные указатели и удалить их с помощью удалить .
Но это не имеет смысла и только замедлит дело.
Операционная система по-прежнему освободит всю память, которую использовал процесс, и сделает это почти мгновенно.
Не удалять объекты безопасно с практической точки зрения.
Все эти «забытые» объекты в памяти не содержат никаких финализаторов.
Их деструкторы не отображают сообщения, не пишут логи, не удаляют файлы и так далее.
Это очень простые классы, которые содержат лишь несколько чисел, строк и указателей/ссылок на другие подобные объекты.
Итак, поскольку каждый процесс обрабатывает одну единицу компиляции, вам не придется беспокоиться о том, нужны ли в процессе те или иные данные или нет. Легче сохранить все до конца.
Это увеличивает потребление памяти, но с точки зрения современной компьютерной техники эти объемы не критичны.
Но это немного упрощает разработку и сокращает время работы.
По нашим приблизительным замерам, если в конце самостоятельно освободить память, скорость замедлится примерно на 5%.
Обработка внутренних ошибок
Что произойдет, если память закончится? Поскольку каждый файл обрабатывается отдельно, сбой в одном из процессов мало влияет на общую картину.Сам сбой может произойти по разным причинам.
Например, анализируемый файл может содержать некомпилированный код или даже мусор.
И тогда, например, один из процессов может начать потреблять слишком много памяти или работать недопустимо долго ( V006 ).
В случае такого неблагоприятного события процесс будет просто убит, а анализ проекта продолжится.
Процесс не содержит никакой специальной информации, которая не может быть потеряна.
Да, неприятно, что анализатор не выдает никаких предупреждений, но это не критично.
Итак, что будет, если не хватит памяти и повторный звонок оператору новый выдаст исключение станд::bad_alloc ? Исключение будет перехвачено на верхнем уровне, и ядро спокойно завершит работу с соответствующим предупреждением.
Такой подход к обработке внутренних ошибок может показаться грубым.
Но на практике сбои случаются крайне редко, и лучше остановиться, чем пытаться разобраться в ситуации, когда все идет не так.
Сбои обычно вызваны ситуацией, когда анализатор столкнулся с чем-то необычным и нестандартным.
Остановиться на таких входных данных — вполне рациональное решение.
Конечно, без практических примеров объяснить сложно.
Поэтому предлагаю вашему вниманию юмористический репортаж моего коллеги.
Речь идет о паре случаев потребления большого объема памяти с последующей остановкой процессов по таймауту.
В истории фигурируют такие интересные сущности, как строковые литералы размером 26 мегабайт и функция длиной более 800 000 строк.
Юрий Минаев.
Конференция CoreHard 2019. Не связывайтесь с поддержкой программистов C++ .
Дополнительные ссылки
- Оптимизация приложений .
NET: большие результаты от небольших изменений
. - Оптимизация .
NET-приложения: как простые правки помогли ускорить PVS-Studio и снизить потребление памяти на 70%
. - Подводные камни при работе с перечислением в C# .
- Межмодульный анализ C++-проектов в PVS-Studio .
- Технологии статического анализа кода PVS-Studio .
Какую стратегию освобождения памяти использует ядро PVS-Studio C и C++? .
Теги: #программирование #C++ #статический анализ кода #управление памятью #pvs-studio #управление памятью в c++
-
Linux В Школах?
19 Oct, 24 -
«Как Близнецы»: 3 Пары Похожих Терминов Itil
19 Oct, 24 -
Счетчик Просмотров На Ютубе
19 Oct, 24