Как я могу узнать, какие части в коде никогда не используются?

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

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

19 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

По документации:

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

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

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

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

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

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

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

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

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

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 function, или для каждой функции в math.h, или для любой функции с фиксированным вводом, чью область она может выяснить. Это становится очень, очень сложным, и сложность в основном безгранична. Вы можете продолжать добавлять уровни сложности к своему компилятору, но всегда будет способ проникнуть в некоторый код, который будет недоступен для любого данного набора входных данных.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отказ от ответственности: я не работаю там, но у меня есть лицензия на этот инструмент (а также NDepend, который является более мощной альтернативой для кода.NET).

Для тех, кому интересно, вот пример встроенного (настраиваемого) правила для обнаружения мертвых методов, написанного на CQLinq:

// <Name>Potentially dead Methods</Name>
warnif count > 0
// Filter procedure for methods that should'nt be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
    m => !m.IsPublic &&       // Public methods might be used by client applications of your Projects.
         !m.IsEntryPoint &&            // Main() method is not used by-design.
         !m.IsClassConstructor &&      
         !m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
         !(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
           m.IsProtected) &&
         !m.IsGeneratedByCompiler
)

// Get methods unused
let methodsUnused = 
   from m in JustMyCode.Methods where 
   m.NbMethodsCallingMe == 0 && 
   canMethodBeConsideredAsDeadProc(m)
   select m

// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
   methods => // Unique loop, just to let a chance to build the hashset.
              from o in new[] { new object() }
              // Use a hashet to make Intersect calls much faster!
              let hashset = methods.ToHashSet()
              from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
              where canMethodBeConsideredAsDeadProc(m) &&
                    // Select methods called only by methods already considered as dead
                    hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe
              select m)

from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
select new { m, m.MethodsCallingMe, depth = deadMethodsMetric[m] }

Одним из способов является использование отладчика и функции компилятора для устранения неиспользуемого машинного кода во время компиляции.

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

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

Это много работы, но я думаю, быстрее, чем анализировать весь код вручную.

У меня был друг, задающий мне этот вопрос сегодня, и я оглянулся на некоторые многообещающие разработки Clang, например, ASTMatcher и Static Analyzer, которые могут иметь достаточную наглядность в последующих действиях во время компиляции, чтобы определить разделы мертвого кода, но затем я нашел это:

https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables

Это в значительной степени полное описание того, как использовать несколько флагов GCC, которые, по-видимому, предназначены для идентификации символов без ссылок!

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

Например, если вы используете Visual Studio, вы можете использовать такой инструмент, как .NET ANTS Profiler, который может анализировать и профилировать ваш код. Таким образом, вы должны быстро узнать, какая часть вашего кода фактически используется. Eclipse также имеет эквивалентные плагины.

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

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

Я не думаю, что это можно сделать автоматически.

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

Может быть очень сложным и дорогостоящим инструментом статического анализа, таким как компилятор Coverity или LLVM.

Но я не уверен и предпочел бы ручную проверку кода.

ОБНОВЛЕНО

Хорошо... только удаление неиспользуемых переменных, неиспользуемые функции не сложно, хотя.

ОБНОВЛЕНО

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

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

Компоновщик GNU имеет --crefопция, которая создает информацию о перекрестных ссылках. Вы можете передать это из gccкомандная строка через -Wl,--cref.

Например, предположим, что определяет символ, который также используется в bar.o. Тогда в выводе вы увидите:

      foo_sym                            foo.o
                                   bar.o

Если ограничен , то вы не увидите никаких дополнительных объектных файлов; за ним будет следовать другой символ:

      foo_sym                            foo.o
force_flag                         options.o

Теперь из этого мы не знаем, что не используется. Это всего лишь кандидат: мы знаем, что он определен в одном файле и не используется ни в каких других. foo_symможно определить в foo.oи используется там.

Итак, что вы делаете с этой информацией?

  1. Поработайте с текстом, чтобы идентифицировать эти символы, ограниченные одним объектным файлом, и создайте список кандидатов.
  2. Зайдите в исходный код и дайте каждому из кандидатов внутреннюю ссылку с static, как и должно быть.
  3. Перекомпилируйте исходники.
  4. Теперь для любого из тех символов, которые действительно не используются, компилятор сможет предупредить, точно указав их для вас; вы можете удалить их.

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

Общая проблема, если какая-то функция будет вызвана, является NP-Complete. Вы не можете заранее знать, будет ли вызвана какая-либо функция, поскольку вы не будете знать, остановится ли когда-нибудь машина Тьюринга. Вы можете получить, если есть некоторый (статически) путь, который идет от main() к функции, которую вы написали, но это не гарантирует, что она когда-либо будет вызвана.

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

По документации:

Warn whenever a variable is unused aside from its declaration, whenever a function is declared static but never defined, whenever a label is declared but not used, and whenever a statement computes a result that is explicitly not used.

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

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

This option is intended to warn when the compiler detects that at least a whole line of source code will never be executed, because some condition is never satisfied or because it is after a procedure that never returns.
Другие вопросы по тегам