У меня есть устаревший код на С++, из которого я должен удалить неиспользуемый код. Проблема в том, что база кода большая.
Как узнать, какой код никогда не вызывается/не используется?
У меня есть устаревший код на С++, из которого я должен удалить неиспользуемый код. Проблема в том, что база кода большая.
Как узнать, какой код никогда не вызывается/не используется?
Существует два варианта неиспользуемого кода:
Для первого рода хороший компилятор может помочь:
-Wunused
(GCC, Clang) должен предупредить о неиспользуемых переменных, Clang unused analyzer даже был увеличен, чтобы предупреждать о переменных, которые никогда не читаются (хотя и используются).-Wunreachable-code
(более старый GCC, удален в 2010 году) должен предупредить о локальных блоках, которые никогда не доступны (это происходит с ранними возвращениями или условиями которые всегда оцениваются как истинные)catch
, потому что компилятор вообще не может доказать, что исключение не будет выбрано.Для второго рода это намного сложнее. Статически это требует анализа всей программы, и даже несмотря на то, что оптимизация времени ссылки может фактически удалить мертвый код, на практике программа была так сильно преобразована во время ее выполнения, что почти невозможно передать значимую информацию пользователю.
Таким образом, существуют два подхода:
gcov
). Обратите внимание, что во время компиляции необходимо передать определенные флаги, чтобы он работал правильно). Вы запускаете инструмент покрытия кода с хорошим набором разнообразных входных данных (ваши модульные тесты или тесты без регрессии), мертвый код обязательно находится в пределах незашифрованного кода... и поэтому вы можете начать здесь.Если вы очень заинтересованы в предмете, и у вас есть время и желание на самом деле разработать инструмент самостоятельно, я бы предложил использовать библиотеки Clang для создания такого инструмента.
Поскольку Clang будет анализировать код для вас и выполнять разрешение перегрузки, вам не придется иметь дело с правилами языка С++, и вы сможете сосредоточиться на проблеме.
Однако этот вид техники не может идентифицировать виртуальные переопределения, которые не используются, поскольку они могут быть вызваны сторонним кодом, о котором вы не можете рассуждать.
В случае неиспользуемых целых функций (и неиспользуемых глобальных переменных) GCC может фактически выполнить большую часть работы для вас, если вы используете GCC и GNU ld.
При компиляции источника используйте -ffunction-sections
и -fdata-sections
, затем при связывании используйте -Wl,--gc-sections,--print-gc-sections
. Теперь компоновщик перечисляет все функции, которые могут быть удалены, потому что они никогда не вызывались и все глобальные переменные, на которые не ссылались.
(Конечно, вы также можете пропустить часть --print-gc-sections
и позволить компоновщику отключить функции молча, но сохраните их в источнике.)
Примечание: это приведет к поиску неиспользуемых полных функций, но ничего не сделает о мертвом коде в функциях. Функции, вызываемые из мертвого кода в живых функциях, также будут поддерживаться.
Некоторые С++-специфические функции также вызовут проблемы, в частности:
В обоих случаях все, что используется виртуальной функцией или конструктором глобальной переменной, также необходимо поддерживать.
Дополнительное предупреждение состоит в том, что если вы создаете общую библиотеку, настройки по умолчанию в GCC будут экспортировать каждую функцию в общую библиотеку, что приведет к ее "использованию" в отношении компоновщика. Чтобы исправить это, вам нужно установить по умолчанию для скрытия символов вместо экспорта (используя, например, -fvisibility=hidden
), а затем явно выбрать экспортированные функции, которые необходимо экспортировать.
Хорошо, если вы используете g++, вы можете использовать этот флаг -Wunused
Согласно документации:
Предупреждать, когда переменная не используется кроме его заявления, когда функция объявляется статической, но никогда не определяется, когда метка объявляется, но не используется, и всякий раз, когда оператор вычисляет результат, который явно не используется.
http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html
Изменить. Вот еще один полезный флаг -Wunreachable-code
Согласно документации:
Этот параметр предназначен для предупреждения, когда компилятор обнаруживает, что по крайней мере целая строка исходного кода никогда не будет выполнена, потому что какое-то условие никогда не выполняется или потому, что это после процедуры, которая никогда не возвращается.
Обновление: я нашел похожую тему обнаружение мертвого кода в устаревшем проекте C/С++
Я думаю, что вы ищете инструмент код покрытия. Инструмент покрытия кода будет анализировать ваш код во время его работы, и он позволит вам узнать, какие строки кода были выполнены и сколько раз, а какие нет.
Вы можете попробовать дать этому инструменту покрытия открытого кода возможность: TestCocoon - инструмент для покрытия кода для 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
или для каждой функции в math.h или для любой функции с фиксированным входом, домен которой он может определить. Это становится очень, очень сложным, и сложность в основном безгранична. Вы можете добавлять уровни сложности в свой компилятор, но всегда будет способ проникнуть в некоторый код, который будет недоступен для любого заданного набора входов.
И тогда есть наборы ввода, которые просто никогда не вводятся. Ввод, который не имеет смысла в реальной жизни, или блокируется логикой проверки в другом месте. Компилятор не знает о них.
Конечным результатом этого является то, что, хотя упомянутые другие программные средства чрезвычайно полезны, вы никогда не узнаете наверняка, что вы поймали все, пока вы не перейдете через код вручную после этого. Даже тогда вы никогда не будете уверены, что ничего не пропустите.
Единственное реальное решение, IMHO, должно быть настолько бдительным, насколько это возможно, использовать автоматизацию в вашем распоряжении, рефакторинг, где вы можете, и постоянно искать способы улучшить свой код. Конечно, в любом случае это хорошая идея.
Я не использовал его сам, но cppcheck, утверждает, что находит неиспользуемые функции. Вероятно, это не решит проблему, но может начаться.
Вы можете попробовать использовать PC-lint/FlexeLint из Gimple Software. Он утверждает, что
найти неиспользуемые макросы, typedef, классы, участники, декларации и т.д. по всему проекту
Я использовал его для статического анализа и нашел его очень хорошим, но я должен признать, что я не использовал его для поиска мертвого кода.
Мой обычный подход к поиску неиспользуемого материала -
watch "make 2>&1"
имеет тенденцию делать трюк в Unix.Это несколько длительный процесс, но он дает хорошие результаты.
Отметьте как можно больше публичных функций и переменных как частных или защищенных, не вызывая ошибки компиляции, при этом попытайтесь также реорганизовать код. Предоставляя частные функции и в какой-то степени защищенные, вы уменьшили область поиска, так как частные функции можно вызывать только из одного класса (если нет глупых макросов или других трюков, чтобы обойти ограничение доступа, и если в этом случае я рекомендую вам найти новую работу). Намного легче определить, что вам не нужна частная функция, так как только класс, в котором вы сейчас работаете, может вызвать эту функцию. Этот метод проще, если ваша база кода имеет небольшие классы и слабо связана. Если ваша база кода не имеет небольших классов или имеет очень плотную связь, я предлагаю сначала очистить их.
Далее следует отметить все остальные публичные функции и составить граф вызовов, чтобы выяснить взаимосвязь между классами. Из этого дерева попытайтесь выяснить, какая часть ветки выглядит так, как ее можно обрезать.
Преимущество этого метода состоит в том, что вы можете делать это на основе каждого модуля, поэтому легко продолжать передавать ваш unittest без значительного периода времени, когда у вас сломанная база кода.
Я действительно не использовал инструмент, который делает такую вещь... Но, насколько я видел во всех ответах, никто никогда не говорил, что эта проблема невыполнима.
Что я имею в виду? Эта проблема не может быть решена каким-либо алгоритмом на компьютере. Эта теорема (что такой алгоритм не существует) является следствием проблемы остановки Тьюринга.
Все инструменты, которые вы будете использовать, - это не алгоритмы, а эвристики (то есть не точные алгоритмы). Они не дадут вам точно весь код, который не используется.
Если вы находитесь в Linux, вам может понадобиться изучить callgrind
, инструмент анализа программ на C/С++, который является частью набора valgrind
, который также содержит инструменты, которые проверяют утечку памяти и другие ошибки памяти ( которые вы также должны использовать). Он анализирует исполняемый экземпляр вашей программы и производит данные о его графике вызовов и о затратах на производительность узлов на графике вызовов. Он обычно используется для анализа производительности, но также создает граф вызовов для ваших приложений, поэтому вы можете видеть, какие функции вызывают, а также их вызывающие.
Это, очевидно, дополняет статические методы, упомянутые в другом месте на странице, и это будет полезно только для устранения полностью неиспользуемых классов, методов и функций - это не поможет найти мертвый код внутри методов, которые на самом деле называются.
Один из способов - использовать отладчик и функцию компилятора для исключения неиспользуемого машинного кода во время компиляции.
После устранения какого-либо машинного кода отладчик не позволит вам помещать breakpojnt в соответствующую строку исходного кода. Таким образом, вы помещаете точки останова повсюду и запускаете программу и проверяете точки останова - те, которые находятся в состоянии "без кода, загруженного для этого источника", соответствуют исключенному коду - либо этот код никогда не вызывается, либо он не был встроен, и вам нужно выполнить минимум анализ, чтобы выяснить, какая из этих двух произошла.
По крайней мере, как это работает в Visual Studio, и я думаю, что другие инструменты также могут это сделать.
Это много работы, но я думаю быстрее, чем вручную анализировать весь код.
Это зависит от платформы, которую вы используете для создания своего приложения.
Например, если вы используете Visual Studio, вы можете использовать инструмент, например .NET ANTS Profiler, который способен анализировать и профилировать ваш код, Таким образом, вы должны быстро узнать, какая часть вашего кода используется на самом деле. Eclipse также имеет эквивалентные плагины.
В противном случае, если вам нужно знать, какая функция вашего приложения фактически используется вашим конечным пользователем, и если вы можете легко освободить приложение, вы можете использовать файл журнала для аудита.
Для каждой основной функции вы можете отслеживать ее использование, и через несколько дней/неделю просто получите этот файл журнала и посмотрите на него.
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] }
Я не думаю, что это можно сделать автоматически.
Даже при использовании инструментов покрытия кода вам необходимо предоставить достаточные входные данные для запуска.
Может быть очень сложным и дорогостоящим инструментом статического анализа, например, Coverity или Компилятор LLVM может помочь.
Но я не уверен, и я бы предпочел ручной обзор кода.
ОБНОВЛЕНО
Ну.. только удаление неиспользуемых переменных, неиспользуемые функции не сложно.
ОБНОВЛЕНО
После прочтения других ответов и комментариев я более убежден, что это невозможно.
Вы должны знать, что код имеет значимую меру покрытия кода, и если вы знаете, что много ручного редактирования будет быстрее, чем подготовить/запустить/просмотреть результаты покрытия.
У меня был друг, который задал мне этот вопрос сегодня, и я оглянулся на некоторые многообещающие события Кланг, например. ASTMatcher и Статический анализатор, который может иметь достаточную видимость во время компиляции для определения разделов мертвого кода, но затем я нашел это:
https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables
Это довольно подробное описание того, как использовать несколько флагов GCC, которые, по-видимому, предназначены для идентификации неподписанных символов!
Общая проблема, если какая-либо функция будет вызываться, 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
Изменить: Вот еще один полезный флаг -Универсабельный код Согласно документации:
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.