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


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

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

18 301

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++функции также вызовут проблемы, в частности:

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

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

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

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

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

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

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

Edit: вот еще один полезный флаг -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 для набора целых чисел (жесткий). Очень сложный компилятор может сделать это для

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

например, если вы используете Visual Studio, вы можете использовать такой инструмент, как.NET ANTS Profiler который способен анализировать и профилировать ваш код. Таким образом, вы должны быстро узнать, какая часть вашего кода фактически используется. 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.