Как я могу узнать, какие части в коде никогда не используются? - PullRequest
307 голосов
/ 27 января 2011

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

Как узнать, какой код никогда не вызывается / не используется?

Ответы [ 18 ]

195 голосов
/ 27 января 2011

Существует две разновидности неиспользуемого кода:

  • локальный, то есть в некоторых функциях некоторые пути или переменные не используются (или используются, но бессмысленно, как написано, но никогда не читается)
  • глобальный: функции, которые никогда не вызываются, глобальные объекты, к которым никогда не осуществляется доступ

Для первого типа, хороший компилятор может помочь:

  • -Wunused (GCC, Clang ) должен предупреждать о неиспользуемых переменных, даже неиспользуемый анализатор Clang был увеличен для предупреждения о переменных, которые никогда не читаются (даже если используются).
  • -Wunreachable-code (более старый GCC, удален в 2010 ) должен предупреждать о локальных блоках, к которым никогда не осуществляется доступ (это происходит с ранним возвратом или условиями, которые всегда оцениваются как true)
  • я не знаю ни одной опции для предупреждения о неиспользуемых catch блоках, потому что компилятор обычно не может доказать, что не будет выброшено исключение.

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

Поэтому существует два подхода:

  • Теоретическим является использование статического анализатора. Часть программного обеспечения, которая детально изучит весь код сразу и найдет все пути потока. На практике я не знаю ни одного, который бы работал здесь.
  • Прагматичным является использование эвристики: используйте инструмент покрытия кода (в цепочке GNU это gcov. Обратите внимание, что для правильной работы должны быть переданы определенные флаги во время компиляции). Вы запускаете инструмент покрытия кода с хорошим набором различных входных данных (ваши юнит-тесты или нерегрессионные тесты), мертвый код обязательно находится в недоступном коде ... и поэтому вы можете начать отсюда.

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

  1. Использование библиотеки Clang для получения AST (абстрактного синтаксического дерева)
  2. Выполнить анализ меток и разверток, начиная с точек входа и далее

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

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

34 голосов
/ 01 февраля 2011

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

При компиляции источника используйте -ffunction-sections и -fdata-sections, затем при компоновке используйте -Wl,--gc-sections,--print-gc-sections. Компоновщик теперь перечислит все функции, которые могут быть удалены, потому что они никогда не вызывались, и все глобальные переменные, на которые никогда не ссылались.

(Конечно, вы также можете пропустить часть --print-gc-sections и позволить компоновщику удалить функции без уведомления, но сохранить их в источнике.)

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

Некоторые специфичные для C ++ функции также могут вызывать проблемы, в частности:

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

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

Дополнительным предостережением является то, что если вы создаете общую библиотеку, настройки по умолчанию в GCC будут экспортировать каждую функцию в общей библиотеке, вызывая ее "использование" до тех пор, пока компоновщик обеспокоен. Чтобы это исправить, вам нужно установить по умолчанию скрытие символов вместо экспорта (например, -fvisibility=hidden), а затем явно выбрать экспортируемые функции, которые нужно экспортировать.

25 голосов
/ 27 января 2011

Хорошо, если вы используете g ++, вы можете использовать этот флаг -Wunused

Согласно документации:

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

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

Редактировать : вот другой полезный флаг -Wunreachable-code Согласно документации:

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

Обновление : я нашел похожую тему Обнаружение мертвого кода в устаревшем проекте C / C ++

18 голосов
/ 27 января 2011

Я думаю, вы ищете инструмент покрытие кода .Инструмент покрытия кода проанализирует ваш код во время его работы и даст вам знать, какие строки кода были выполнены и сколько раз, а какие - нет.

Вы можете попробовать открыть этот код.у инструмента покрытия исходного кода есть шанс: TestCocoon - инструмент покрытия кода для C / C ++ и C #.

15 голосов
/ 01 февраля 2011

Реальный ответ здесь: Вы никогда не можете знать наверняка.

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

double x = sqrt(2);
if (x > 5)
{
  doStuff();
}

Как правильно отмечает Википедия, умный компилятор может поймать что-то подобное. Но рассмотрим модификацию:

int y;
cin >> y;
double x = sqrt((double)y);

if (x != 0 && x < 1)
{
  doStuff();
}

Компилятор поймает это? Может быть. Но для этого нужно будет сделать больше, чем запустить sqrt с постоянным скалярным значением. Необходимо будет выяснить, что (double)y всегда будет целым числом (легко), а затем понять математический диапазон sqrt для набора целых чисел (трудно). Очень сложный компилятор может сделать это для функции sqrt, или для каждой функции в math.h , или для любой функции с фиксированным вводом, чью область он может выяснить. Это становится очень, очень сложным, и сложность в основном безгранична. Вы можете продолжать добавлять уровни сложности к своему компилятору, но всегда будет способ проникнуть в некоторый код, который будет недоступен для любого заданного набора входных данных.

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

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

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

12 голосов
/ 27 января 2011

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

9 голосов
/ 27 января 2011

Вы можете попробовать использовать PC-lint / FlexeLint от Gimple Software .Он утверждает, что

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

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

4 голосов
/ 27 января 2011

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

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

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

4 голосов
/ 27 января 2011

Мой обычный подход к поиску неиспользуемых вещей -

  1. убедитесь, что система сборки правильно обрабатывает отслеживание зависимостей
  2. настроить второй монитор с полноэкранным окном терминала, запустить повторные сборки и показать первый скриншот вывода. watch "make 2>&1" имеет тенденцию добиваться успеха в Unix.
  3. запустить операцию поиска и замены для всего дерева исходных текстов, добавив "??" В начале каждой строки
  4. исправить первую ошибку, помеченную компилятором, удалив "//?" в соответствующих строках.
  5. Повторяйте, пока не останется ошибок.

Это довольно длительный процесс, но он дает хорошие результаты.

3 голосов
/ 27 января 2011

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

Что я имею в виду под этим? Эта проблема не может быть решена никаким алгоритмом на компьютере. Эта теорема (о том, что такого алгоритма не существует) является следствием проблемы останова Тьюринга.

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

...