development

코드에서 어떤 부분이 사용되지 않는지 어떻게 알 수 있습니까?

big-blog 2020. 3. 11. 08:16
반응형

코드에서 어떤 부분이 사용되지 않는지 어떻게 알 수 있습니까?


사용하지 않는 코드를 제거 해야하는 레거시 C ++ 코드가 있습니다. 문제는 코드 기반이 크다는 것입니다.

어떤 코드가 절대 호출 / 사용되지 않는지 어떻게 알 수 있습니까?


사용되지 않는 코드에는 두 가지 종류가 있습니다.

  • 로컬 함수, 즉 일부 함수에서는 일부 경로 또는 변수가 사용되지 않습니다 (또는 쓰지만 읽지 않는 것과 같이 의미있는 방식으로 사용되지 않음)
  • 글로벌 함수 : 절대 호출되지 않는 함수, 절대 액세스 할 수없는 글로벌 오브젝트

첫 번째 종류의 경우 좋은 컴파일러가 도움이 될 수 있습니다.

  • -Wunused(GCC, Clang )는 미사용 변수에 대해 경고해야하며, Clang 미사용 분석기는 읽지 않은 변수에 대해 경고하기 위해 증가되었습니다 (사용 되더라도).
  • -Wunreachable-code(이전 GCC, 2010 에서 제거됨 )는 액세스하지 않은 로컬 블록에 대해 경고해야합니다 (항상 true로 평가되는 초기 반환 또는 조건에서 발생)
  • catch컴파일러가 일반적으로 예외가 발생하지 않는다는 것을 증명할 수 없기 때문에 사용하지 않는 블록 에 대해 경고하는 옵션은 없습니다 .

두 번째 종류의 경우 훨씬 더 어렵습니다. 정적으로 전체 프로그램 분석이 필요하며 링크 시간 최적화가 실제로 데드 코드를 제거 할 수는 있지만 실제로 프로그램은 수행 될 때 너무 많이 변환되어 의미있는 정보를 사용자에게 전달하는 것이 거의 불가능합니다.

따라서 두 가지 접근 방식이 있습니다.

  • 이론적 인 것은 정적 분석기를 사용하는 것입니다. 전체 코드를 한 번에 자세히 검사하고 모든 흐름 경로를 찾는 소프트웨어입니다. 실제로 나는 여기서 작동하는 것을 모른다.
  • 실용적인 방법은 휴리스틱을 사용하는 것입니다. 코드 커버리지 도구를 사용하십시오 (GNU 체인에서는 gcov. 컴파일 중에는 특정 플래그를 전달해야 제대로 작동합니다). 다양한 범위의 입력 (단위 테스트 또는 회귀 테스트가 아님)으로 코드 적용 범위 도구를 실행하면 데드 코드는 반드시 도달 할 수없는 코드 내에 있으므로 여기에서 시작할 수 있습니다.

주제에 관심이 있고 실제로 도구를 직접 연습 할 시간과 성향이 있다면 Clang 라이브러리를 사용하여 그러한 도구를 만드는 것이 좋습니다.

  1. Clang 라이브러리를 사용하여 AST (추상 구문 트리)를 얻습니다.
  2. 진입 점부터 마크 앤 스윕 분석 수행

Clang은 코드를 구문 분석하고 과부하 해결을 수행하므로 C ++ 언어 규칙을 처리 할 필요가 없으며 당면한 문제에 집중할 수 있습니다.

그러나 이러한 종류의 기술은 사용하지 않는 가상 재정의를 식별 할 수 없습니다. 이유는 신뢰할 수없는 타사 코드에 의해 호출 될 수 있기 때문입니다.


사용하지 않는 전체 함수 (및 사용되지 않은 전역 변수)의 경우 GCC와 GNU ld를 사용하는 경우 GCC는 실제로 대부분의 작업을 수행 할 수 있습니다.

소스를 컴파일 할 때는 -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 # 용 코드 커버리지 툴을 제공 할 수 있습니다.


실제 답변은 다음과 같습니다. 절대 확실하게 알 수 없습니다.

최소한 사소한 경우에는 모든 것을 얻었을 수도 있습니다. 도달 할 수없는 코드에 대한 Wikipedia의 기사에서 다음을 고려하십시오 .

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

Wikipedia가 올바르게 지적했듯이 영리한 컴파일러는 이와 같은 것을 잡을 수 있습니다. 그러나 수정을 고려하십시오.

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

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

컴파일러가 이것을 잡을까요? 아마도. 그러나이를 위해서는 sqrt상수 스칼라 값에 대해 실행 하는 것 이상을 수행해야 합니다. 그것은 그 파악해야한다 (double)y항상 정수 (쉬운) 될 것이며, 다음의 수학 범위 이해 sqrt정수 (하드)의 세트를. 매우 복잡한 컴파일러는 sqrt함수 또는 math.h의 모든 함수 또는 도메인을 파악할 수있는 고정 입력 함수에 대해이 작업을 수행 할 수 있습니다. 이것은 매우 복잡해지며 복잡성은 기본적으로 무한합니다. 컴파일러에 정교함을 계속 추가 할 수 있지만 주어진 입력 세트에 도달 할 수없는 일부 코드를 몰래 숨길 수있는 방법이 항상 있습니다.

그리고 입력 되지 않은 입력 세트 가 있습니다. 실생활에서 의미가 없거나 다른 곳의 검증 로직에 의해 차단되는 입력. 컴파일러가 이에 대해 알 수있는 방법이 없습니다.

이것의 최종 결과는 다른 사람들이 언급 한 소프트웨어 도구가 매우 유용하지만 나중에 수동으로 코드를 거치지 않으면 모든 것을 포착했는지 확실하게 알 수 없다는 것입니다. 그럼에도 불구하고, 당신은 당신이 아무것도 놓치지 않았다고 확신하지 못할 것입니다.

유일한 실제 솔루션 인 IMHO는 가능한 한주의를 기울이고, 원하는대로 자동화를 사용하고, 가능한 경우 리팩터링하며, 코드를 개선 할 수있는 방법을 지속적으로 찾는 것입니다. 물론, 어쨌든 그렇게하는 것이 좋습니다.


나는 그것을 직접 사용하지는 않았지만 cppcheck 는 사용되지 않는 기능을 찾는 다고 주장합니다. 완전한 문제를 해결하지 못할 수도 있지만 시작일 수도 있습니다.


Gimple Software에서 PC-lint / FlexeLint를 사용해 볼 수 있습니다. 그것은 주장

전체 프로젝트에서 사용되지 않는 매크로, typedef, 클래스, 멤버, 선언 등을 찾습니다.

나는 정적 분석을 위해 그것을 사용했고 그것을 매우 좋아한다는 것을 알았지 만 죽은 코드를 찾기 위해 그것을 사용하지 않았다는 것을 인정해야합니다.


사용하지 않는 물건을 찾는 일반적인 방법은

  1. 빌드 시스템이 종속성 추적을 올바르게 처리하는지 확인하십시오.
  2. 전체 화면 터미널 창이있는 두 번째 모니터를 설정하고 반복 빌드를 실행하고 첫 번째 화면 출력을 표시합니다. watch "make 2>&1"유닉스에서 트릭을 수행하는 경향이 있습니다.
  3. 모든 줄의 시작 부분에 "//?"를 추가하여 전체 소스 트리에서 찾기 및 바꾸기 작업을 실행합니다.
  4. "//?"를 제거하여 컴파일러가 표시 한 첫 번째 오류를 수정하십시오. 해당 줄에.
  5. 남은 오류가 없을 때까지 반복하십시오.

이것은 다소 긴 과정이지만 좋은 결과를 제공합니다.


컴파일 오류를 발생시키지 않으면 서 많은 공용 함수 및 변수를 개인용 또는 보호 된 것으로 표시하십시오. 이렇게하는 동안 코드도 리팩토링하십시오. 기능을 비공개로 만들고 어느 정도 보호하면 검색 기능이 줄어 듭니다. 비공개 기능은 동일한 클래스에서만 호출 할 수 있기 때문입니다 (액세스 제한을 우회하는 어리석은 매크로 또는 기타 트릭이없는 경우가 아니라면 권장합니다) 새로운 직업을 찾으십시오). 현재 작업중 인 클래스만이 함수를 호출 할 수 있으므로 개인 함수가 필요하지 않다는 것을 결정하는 것이 훨씬 쉽습니다. 코드 기반에 작은 클래스가 있고 느슨하게 연결된 경우이 방법이 더 쉽습니다. 코드베이스에 작은 클래스가 없거나 결합이 매우 타이트한 경우 먼저 정리하는 것이 좋습니다.

다음으로 나머지 모든 공용 함수를 표시하고 클래스 간의 관계를 파악하기 위해 호출 그래프를 작성합니다. 이 나무에서 가지의 어떤 부분이 잘릴 수 있는지 알아 봅니다.

이 방법의 장점은 모듈 단위로 수행 할 수 있다는 점입니다. 따라서 코드 기반이 깨졌을 때 오랜 시간이 걸리지 않고 단위 테스트를 계속 통과 할 수 있습니다.


Linux를 callgrind사용하는 경우 valgrind제품군의 일부인 C / C ++ 프로그램 분석 도구 인 메모리 누수 및 기타 메모리 오류 (사용해야하는)를 검사하는 도구가 포함되어 있습니다. 실행중인 프로그램 인스턴스를 분석하고 호출 그래프 및 호출 그래프에서 노드의 성능 비용에 대한 데이터를 생성합니다. 일반적으로 성능 분석에 사용되지만 응용 프로그램에 대한 호출 그래프도 생성하므로 호출자와 함께 어떤 함수가 호출되는지 확인할 수 있습니다.

이것은 페이지의 다른 곳에서 언급 된 정적 메소드를 보완하는 것으로, 완전히 사용되지 않는 클래스, 메소드 및 함수를 제거하는 데 도움이됩니다. 실제로 호출되는 메소드 내에서 죽은 코드를 찾는 데 도움이되지 않습니다.


나는 실제로 그런 일을하는 도구를 사용하지 않았습니다 ...하지만 모든 대답에서 본 한, 아무도이 문제를 계산할 수 없다고 말한 적이 없습니다.

이것이 무엇을 의미합니까? 이 문제는 컴퓨터의 알고리즘으로 해결할 수 없습니다. 이 정리 (이러한 알고리즘이 존재하지 않음)는 Turing의 Halting Problem의 결과입니다.

사용할 모든 도구는 알고리즘이 아니라 휴리스틱입니다 (예 : 정확한 알고리즘이 아님). 사용되지 않은 모든 코드를 정확하게 제공하지는 않습니다.


한 가지 방법은 컴파일 중에 사용되지 않는 기계 코드를 제거하는 디버거 및 컴파일러 기능을 사용하는 것입니다.

일부 기계 코드가 제거되면 디버거에서 해당 소스 코드 행에 breakpojnt를 넣을 수 없습니다. 따라서 중단 점을 어디에나 배치하고 프로그램을 시작하고 중단 점을 검사하십시오. "이 소스에 대해로드 된 코드가 없음"상태 인 코드는 제거 된 코드에 해당합니다. 코드가 호출되지 않았거나 인라인되지 않았으며 최소값을 수행해야합니다. 이 두 가지 중 어느 것이 일어 났는지 찾아 내야합니다.

적어도 그것이 Visual Studio에서 작동하는 방식이며 다른 도구 세트도 그렇게 할 수 있다고 생각합니다.

많은 작업이지만 수동으로 모든 코드를 분석하는 것보다 빠릅니다.


응용 프로그램을 만드는 데 사용하는 플랫폼에 따라 다릅니다.

예를 들어 Visual Studio를 사용하는 경우 코드를 구문 분석하고 프로파일 링 할 수있는 .NET ANTS 프로파일 러 와 같은 도구를 사용할 수 있습니다 . 이런 식으로 코드의 어느 부분이 실제로 사용되는지 신속하게 알아야합니다. Eclipse에는 동등한 플러그인이 있습니다.

그렇지 않으면 최종 사용자가 실제로 사용하는 응용 프로그램 기능을 알아야하고 응용 프로그램을 쉽게 해제 할 수있는 경우 감사를 위해 로그 파일을 사용할 수 있습니다.

각 주요 기능에 대해 사용량을 추적 할 수 있으며 며칠 / 주 후에 해당 로그 파일을 가져 와서 살펴보십시오.


CppDepend 는 사용되지 않는 유형, 방법 및 필드를 감지하고 훨씬 더 많은 작업을 수행 할 수있는 상용 도구입니다. Windows 및 Linux에서 사용할 수 있지만 (현재 64 비트 지원은 없음) 2 주 평가판이 제공됩니다.

면책 조항 : 나는 거기에서 일하지 않지만이 도구에 대한 라이센스 ( .NET 코드의 더 강력한 대안 인 NDepend )를 소유 하고 있습니다.

궁금한 사람들을 위해 다음은 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 컴파일러 와 같은 매우 복잡하고 가격이 높은 정적 분석 도구 가 도움이 될 수 있습니다.

그러나 확실하지 않으며 수동 코드 검토를 선호합니다.

업데이트

글쎄 .. 사용하지 않는 변수 만 제거하면 사용되지 않는 함수는 어렵지 않습니다.

업데이트

다른 답변과 의견을 읽은 후에는 할 수 없다는 것을 더 확신합니다.

의미있는 코드 적용 범위 측정을 위해서는 코드를 알아야하며, 수동 편집을 많이 수행하면 적용 범위 결과를 준비 / 실행 / 검토하는 것보다 빠릅니다.


나는 친구에게 오늘이 질문을 나에게 요청했고 , 데드 코드 섹션을 결정하기 위해 컴파일하는 동안 진행 중에 충분한 가시성을 가질 수있는 ASTMatcherStatic Analyzer같은 유망한 Clang 개발을 둘러 보았습니다. 이것을 찾았습니다 :

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

참조되지 않은 심볼을 식별하기 위해 설계된 것처럼 보이는 몇 가지 GCC 플래그를 사용하는 방법에 대한 완전한 설명입니다!


어떤 함수가 호출 될지에 대한 일반적인 문제는 NP-Complete입니다. 튜링 기계가 멈추는 지 알 수 없으므로 어떤 기능이 호출되면 일반적인 방법으로 미리 알 수 없습니다. main ()에서 작성한 함수로가는 경로가 (정적으로) 있으면 얻을 수 있지만 그것이 호출 될 것을 보증하지는 않습니다.


g ++을 사용하면이 플래그를 사용할 수 있습니다-사용하지 않음

문서에 따르면 :

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.

참고 URL : https://stackoverflow.com/questions/4813947/how-can-i-know-which-parts-in-the-code-are-never-used

반응형