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

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

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

18 ответов


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

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

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

  • -Wunused (GCC, лязгом) следует предупредить о неиспользуемых переменных, Clang unused analyzer даже был увеличен, чтобы предупредить о переменных, которые никогда не читаются (хотя и используются).
  • -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++-specific также вызовут проблемы, в частности:

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

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

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


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

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

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

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

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

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

обновление: я нашел аналогичную тему обнаружение мертвого кода в проекте legacy 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();
}

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


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


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

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

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


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

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

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


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

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

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


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

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


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

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

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


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

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

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

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


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

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

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

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


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

отказ от ответственности: я там не работаю, но у меня есть лицензия на этот инструмент (а также вопросом, что происходит, что является более мощной альтернативой для .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] }

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

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

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

но я не уверен, и я бы предпочел ручной анализ кода.

обновлено

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

обновлено

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

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


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

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

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


общая проблема, если будет вызвана некоторая функция, является NP-полной. Вы не можете заранее знать, будет ли вызвана какая-то функция, так как вы не будете знать, остановится ли машина Тьюринга. Вы можете получить, если есть некоторый путь (статически), который идет от 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

Edit: вот другой полезный флаг-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.