metrica
Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Какая стратегия освобождения памяти исп…

Какая стратегия освобождения памяти используется в C и С++ ядре PVS-Studio?

07 Фев 2022

Так получилось, что в различных обсуждениях мы уже несколько раз комментировали, как C и C++ модуль PVS-Studio работает с памятью. А раз так, пришло время оформить этот ответ в виде маленькой статьи.

0916_pvs_studio_memory_allocation_and_free_ru/image1.png

На момент публикации анализатор PVS-Studio содержит в себе три консольных модуля, осуществляющих анализ кода программ на языках:

  • С++, а также на языке C и ряде диалектов, таких как C++/CLI, C++/CX;
  • C#;
  • Java.

Эти модули мы ещё называем ядрами анализатора.

Итак, ядро C# анализатора написано на C#, а Java анализатора — на Java. В этих языках освобождением памяти занимается сборщик мусора, поэтому тут и так всё понятно. Конечно, есть разные моменты, связанные с оптимизацией. Например, в статьях [1, 2, 3] мои коллеги рассматривают уменьшение количества создаваемых временных объектов, настройку сборщика мусора, интернирование и т.д. Но интереснее всего, как же дело обстоит в C и C++ ядре, написанном на C++?

Общий принцип работы ядра

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

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

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

Прежде чем файл начинает анализироваться, происходит его препроцессирование. Для этого используется внешний препроцессор (компилятор). Мы никак не управляем временем работы препроцессора. Условно предположим, что препроцессор работает 3 секунды и ещё 3 секунды выполняется собственно анализ. Добавим ещё одну условную секунду, которая тратится на сбор информации о файле, запуск процессов, чтение файлов и другие не распараллеливаемые или плохо распараллеливаемые операции. Итого 7 секунд.

Представим, что будет реализовано внутреннее распараллеливание и собственно анализ будет выполняться не 3 секунды, а 0.5 секунды. Тогда общее время проверки одного файла сократится с условных 7 секунд до 4.5 секунд. Это приятно, но ничего кардинально не изменилось. При анализе нескольких файлов такое распараллеливание не имеет смысла, так как будет распараллелен анализ файлов, что, кстати, эффективнее. А проверка одного единичного файла, если такое требуется, ускорится несущественно. Зато за это небольшое ускорение придётся заплатить дорогую цену написанием сложного механизма распараллеливания алгоритмов и синхронизации при доступах к общим объектам.

Примечание. А как же PVS-Studio выполняет межмодульный анализ, если каждый процесс обрабатывает только одну единицу компиляции? Анализ выполняется в два прохода. Вначале собирается необходимая информация и записывается в специальный файл. Затем файлы повторно анализируются с учётом ранее собранной информации [4].

Стратегия освобождения памяти

Распараллеливание анализа на уровне обработки файлов имеет ещё одно важное следствие, которое относится уже к теме использования памяти.

Мы не освобождаем память в С++ ядре PVS-Studio до конца выполнения процесса. Это осознанное полезное решение.

0916_pvs_studio_memory_allocation_and_free_ru/image2.png

В общем, наш единорог всегда только жрёт память :).

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

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

Однако есть три вида данных, которые только создаются, но не уничтожаются до конца работы процесса:

  • Абстрактное синтаксическое дерево;
  • Различные данные, собираемые в процессе обхода дерева;
  • "Виртуальные значения", используемые для анализа потока данных и символьного выполнения [5].

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

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

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

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

Обработка внутренних ошибок

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

Сам сбой может произойти по разнообразным причинам. Например, анализируемый файл может содержать некомпилируемый код или вообще мусор. И тогда, например, один из процессов может начать потреблять слишком много памяти или работать недопустимо долго (V006). При таком неблагоприятном событии процесс будет просто убит, а анализ проекта будет продолжен.

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

Итак, что произойдёт, если не хватит памяти и очередной вызов оператора new сгенерирует исключение std::bad_alloc? Исключение будет перехвачено на верхнем уровне, и ядро спокойно завершит работу, выдав соответствующее предупреждение.

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

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

В рассказе фигурируют такие интересные сущности, как строковые литералы на 26 мегабайт и функция длиной более 800 000 строк.

Юрий Минаев. Конференция CoreHard 2019. Не связывайтесь с поддержкой C++ программистов.

Дополнительные ссылки

Популярные статьи по теме
Ошибка настолько проста, что программисты её не замечают

Дата: 12 Сен 2023

Автор: Андрей Карпов

Нам в поддержку написал пользователь о странном ложном срабатывании анализатора PVS-Studio. Сейчас станет понятно, почему этот случай заслуживает отдельной маленькой статьи и насколько у...
Проверка компилятора GCC 13 с помощью PVS-Studio

Дата: 06 Сен 2023

Автор: Игнат Гришечко

После некоторых поисков серьёзного вызова для анализатора PVS-Studio выбор пал на открытую коллекцию компиляторов GCC. Да, это уже не первая по счёту проверка этого проекта. Однако поддерживаемые...
Ква! Как писали код во времена Quake

Дата: 01 Сен 2023

Автор: Антон Третьяков

Как говорил Джон Кармак: "Фокус — это умение определить, на что вы не будете тратить время". Так давайте не будем тратить время на аннотацию и приступим к анализу кода легендарной Quake World.
PVS-Studio vs CodeLite: битва за идеальный код

Дата: 30 Авг 2023

Автор: Евгений Фёклин

Как улучшить качество и надёжность кодовой базы? Один из ответов на этот вопрос — использование статического анализа. В данной статье мы исследуем, как эта методология может улучшить качество кодовой…
Распространённые паттерны опечаток при программировании

Дата: 25 Авг 2023

Автор: Андрей Карпов

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


Комментарии (0)

Следующие комментарии next comments
close comment form