C #에서 작은 코드 샘플을 벤치마킹하면이 구현을 개선 할 수 있습니까?


104

꽤 자주 그래서 나는 어떤 구현이 가장 빠른지 확인하기 위해 작은 코드 덩어리를 벤치마킹합니다.

벤치마킹 코드가 지팅이나 가비지 수집기를 고려하지 않는다는 의견을 자주 봅니다.

천천히 진화 한 다음과 같은 간단한 벤치마킹 기능이 있습니다.

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

용법:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

이 구현에 결함이 있습니까? 구현 X가 Z 반복을 통한 구현 Y보다 빠르다는 것을 보여주는 것으로 충분합니까? 이것을 개선 할 방법을 생각할 수 있습니까?

편집 시간 기반 접근 방식 (반복과 반대)이 선호된다는 것은 꽤 분명합니다. 시간 확인이 성능에 영향을주지 않는 구현이있는 사람이 있습니까?


BenchmarkDotNet을 참조하십시오 .
Ben Hutchison

답변:


95

수정 된 기능은 다음과 같습니다. 커뮤니티에서 권장하는대로 커뮤니티 위키로 수정하십시오.

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

당신이 확인 최적화가 활성화 된 릴리스의 컴파일 및 Visual Studio의 테스트 외부를 실행합니다 . 이 마지막 부분은 JIT가 릴리스 모드에서도 연결된 디버거로 최적화를 표시하기 때문에 중요합니다.


루프 오버 헤드를 최소화하기 위해 10과 같이 몇 번 정도 루프를 펼칠 수 있습니다.
Mike Dunlavey

2
방금 Stopwatch.StartNew를 사용하도록 업데이트했습니다. 기능적 변경은 아니지만 한 줄의 코드를 저장합니다.
LukeH 2009-06-26

1
@Luke, 큰 변화 (+1 할 수 있으면 좋겠다). @Mike im 확실하지 않습니다. 가상 호출 오버 헤드가 비교 및 ​​할당보다 훨씬 높을 것으로 예상되므로 성능 차이는 무시할 수있을 것입니다
Sam Saffron

반복 횟수를 Action에 전달하고 거기에 루프를 생성 할 것을 제안합니다. 상대적으로 짧은 작업을 측정하는 경우 이것이 유일한 옵션입니다. 그리고 역 메트릭을 선호합니다. 예를 들어 초당 패스 수입니다.
Alex Yakunin

2
평균 시간을 표시하는 것에 대해 어떻게 생각하십니까? 다음과 같이됩니다. Console.WriteLine ( "평균 경과 시간 {0} ms", watch.ElapsedMilliseconds / iterations);
rudimenter

22

GC.Collect반품 전에 완료가 반드시 완료되는 것은 아닙니다 . 마무리는 대기열에 추가 된 다음 별도의 스레드에서 실행됩니다. 이 스레드는 테스트 중에 여전히 활성화되어 결과에 영향을 미칠 수 있습니다.

테스트를 시작하기 전에 종료가 완료되었는지 확인하려면을 호출 할 수 있습니다. 그러면 GC.WaitForPendingFinalizers종료 대기열이 지워질 때까지 차단됩니다.

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
GC.Collect()한 번 더?
colinfang 2013 년

7
@colinfang "최종화"중인 객체는 종료 자에 의해 GC 처리되지 않기 때문입니다. 따라서 두 번째 Collect는 "최종화 된"개체도 수집되도록하는 것입니다.
MAV

15

방정식에서 GC 상호 작용을 제거 하려면 GC.Collect 호출 전이 아닌 '준비'호출을 실행하는 것이 좋습니다. 이렇게하면 .NET에 함수의 작업 세트를 위해 OS에서 할당 된 충분한 메모리가 이미 있다는 것을 알 수 있습니다.

각 반복에 대해 인라인되지 않은 메서드 호출을 수행하고 있으므로 테스트중인 항목을 빈 본문과 비교해야합니다. 또한 메서드 호출보다 몇 배 더 긴 시간 만 안정적으로 측정 할 수 있다는 사실을 인정해야합니다.

또한 프로파일 링하는 항목의 종류에 따라 특정 반복 횟수가 아닌 일정 시간 동안 타이밍 기반 실행을 수행 할 수 있습니다. 최상의 구현을 위해서는 매우 짧은 실행을, 최악의 경우에는 매우 긴 실행을해야합니다.


1
좋은 점, 시간 기반 구현을 염두에두고 계십니까?
Sam Saffron

6

나는 대리인을 전혀 전달하지 않을 것입니다.

  1. 위임 호출은 ~ 가상 메서드 호출입니다. 저렴하지 않음 : .NET에서 가장 작은 메모리 할당량의 약 25 %. 자세한 내용은 이 링크를 참조하십시오 .
  2. 익명의 대리인은 클로저 사용으로 이어질 수 있지만 눈치 채지 못할 것입니다. 다시 말하지만, 클로저 필드에 액세스하는 것은 예를 들어 스택의 변수에 액세스하는 것보다 두드러집니다.

클로저 사용으로 이어지는 예제 코드 :

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

클로저에 대해 잘 모르면 .NET Reflector에서이 방법을 살펴보십시오.


흥미로운 점이 있지만 델리게이트를 전달하지 않으면 재사용 가능한 Profile () 메서드를 어떻게 만들 수 있습니까? 임의의 코드를 메서드에 전달하는 다른 방법이 있습니까?
애쉬

1
우리는 "using (new Measurement (...)) {... measurement code ...}"를 사용합니다. 그래서 우리는 델리게이트를 전달하는 대신 IDisposable을 구현하는 Measurement 객체를 얻습니다. code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/…
Alex Yakunin

이것은 폐쇄 문제로 이어지지 않습니다.
Alex Yakunin

3
@AlexYakunin : 링크가 끊어진 것 같습니다. 답변에 측정 클래스의 코드를 포함 할 수 있습니까? 어떻게 구현하든이 IDisposable 접근 방식으로 프로파일 링 할 코드를 여러 번 실행할 수는 없을 것입니다. 그러나 복잡한 (연결된) 응용 프로그램의 여러 부분이 수행되는 방식을 측정하려는 상황에서 실제로 매우 유용합니다. 단, 다른 시간에 실행했을 때 측정 값이 부정확하고 일관성이 없을 수 있다는 점을 염두에두면됩니다. 대부분의 프로젝트에서 동일한 접근 방식을 사용하고 있습니다.
ShdNx

1
성능 테스트를 여러 번 실행해야하는 요구 사항이 정말 중요하므로 (예열 + 여러 측정) 델리게이트도 사용하는 방식으로 전환했습니다. 또한 클로저를 사용하지 않는 경우 델리게이트 호출이 IDisposable.
Alex Yakunin

6

이와 같은 벤치마킹 방법으로 극복하기 가장 어려운 문제는 엣지 케이스와 예상치 못한 상황을 설명하는 것이라고 생각합니다. 예 : "높은 CPU 부하 / 네트워크 사용량 / 디스크 스 래싱 등에서 두 코드 조각이 어떻게 작동합니까?" 특정 알고리즘 이 다른 알고리즘 보다 훨씬 빠르게 작동하는지 확인하기위한 기본 논리 검사에 유용합니다 . 그러나 대부분의 코드 성능을 제대로 테스트하려면 특정 코드의 특정 병목 현상을 측정하는 테스트를 만들어야합니다.

작은 코드 블록을 테스트하는 것은 종종 투자 수익이 거의 없으며 단순한 유지 관리가 가능한 코드 대신 지나치게 복잡한 코드를 사용하도록 장려 할 수 있습니다. 다른 개발자 나 나 자신이 6 개월 후 빠르게 이해할 수있는 명확한 코드를 작성하면 고도로 최적화 된 코드보다 성능상의 이점이 더 많습니다.


1
중요한 것은 실제로로드되는 용어 중 하나입니다. 때로는 20 % 더 빠른 구현이 중요하고 때로는 100 배 더 빨라야 중요합니다. 명확성에 대해 동의하십시오. stackoverflow.com/questions/1018407/…
Sam Saffron

이 경우 중요한 것은로드 된 모든 것이 아닙니다. 하나 이상의 동시 구현을 비교하고 있으며 두 구현의 성능 차이가 통계적으로 중요하지 않은 경우 더 복잡한 방법을 사용할 가치가 없습니다.
폴 알렉산더

5

func()워밍업을 위해 한 번이 아니라 여러 번 전화를 걸었 습니다.


1
의도는 jit 컴파일이 수행되도록하는 것이 었습니다. 측정 전에 func를 여러 번 호출하면 어떤 이점이 있습니까?
Sam Saffron

3
JIT에게 첫 번째 결과를 개선 할 수있는 기회를 제공합니다.
Alexey Romanov

1
.NET JIT는 시간이 지남에 따라 결과를 개선하지 않습니다 (Java와 마찬가지로). 첫 번째 호출에서 한 번만 메서드를 IL에서 Assembly로 변환합니다.
Matt Warren

4

개선을위한 제안

  1. 실행 환경이 벤치마킹에 적합한 지 감지합니다 (예 : 디버거가 연결되어 있는지 또는 잘못된 측정을 초래할 수있는 jit 최적화가 비활성화되었는지 감지).

  2. 코드의 일부를 독립적으로 측정 (병목 지점이 정확히 어디인지 확인)

  3. 다른 버전 / 구성 요소 / 코드 덩어리 비교 (첫 번째 문장에서 '... 어떤 구현이 가장 빠른지 확인하기 위해 작은 코드 덩어리를 벤치마킹'이라고 말합니다.)

# 1에 관하여 :

  • 디버거가 연결되어 있는지 감지하려면 속성을 읽으십시오 System.Diagnostics.Debugger.IsAttached(디버거가 처음에 연결되지 않았지만 얼마 후에 연결되는 경우도 처리해야 함).

  • jit 최적화가 비활성화되어 있는지 감지하려면 DebuggableAttribute.IsJITOptimizerDisabled관련 어셈블리의 속성 을 읽으십시오 .

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }

# 2와 관련하여 :

이것은 여러 가지 방법으로 수행 될 수 있습니다. 한 가지 방법은 여러 델리게이트를 제공 한 다음 해당 델리게이트를 개별적으로 측정하는 것입니다.

# 3에 관하여 :

이것은 또한 여러 가지 방법으로 수행 될 수 있으며 다른 사용 사례는 매우 다른 솔루션을 요구합니다. 벤치 마크를 수동으로 호출하면 콘솔에 쓰는 것이 좋습니다. 그러나 벤치 마크가 빌드 시스템에 의해 자동으로 수행되는 경우 콘솔에 쓰는 것이 좋지 않을 수 있습니다.

이를 수행하는 한 가지 방법은 벤치 마크 결과를 서로 다른 컨텍스트에서 쉽게 사용할 수있는 강력한 형식의 개체로 반환하는 것입니다.


Etimo. 벤치 마크

또 다른 접근 방식은 기존 구성 요소를 사용하여 벤치 마크를 수행하는 것입니다. 실제로 우리 회사에서는 벤치 마크 도구를 공개 도메인에 출시하기로 결정했습니다. 핵심에서 여기에 제시된 다른 답변 중 일부와 마찬가지로 가비지 수집기, 지터, 워밍업 등을 관리합니다. 또한 위에서 제안한 세 가지 기능이 있습니다. Eric Lippert 블로그 에서 논의 된 몇 가지 문제를 관리합니다 .

두 구성 요소를 비교하고 결과를 콘솔에 기록하는 예제 출력입니다. 이 경우 비교되는 두 구성 요소를 'KeyedCollection'및 'MultiplyIndexedKeyedCollection'이라고합니다.

Etimo.Benchmarks-샘플 콘솔 출력

거기에있다 NuGet 패키지 하는 샘플 NuGet 패키지 와 소스 코드에서 확인할 수 있습니다 GitHub의 . 도있다 블로그 게시물 .

급한 경우 샘플 패키지를 받고 필요에 따라 샘플 델리게이트를 수정하는 것이 좋습니다. 서두르지 않으면 블로그 게시물을 읽고 세부 사항을 이해하는 것이 좋습니다.


1

또한 JIT 컴파일러가 코드를 지팅하는 데 소비하는 시간을 제외하려면 실제 측정 전에 "준비"단계를 실행해야합니다.


측정 전에 수행
Sam Saffron

1

벤치마킹하는 코드와 실행되는 플랫폼에 따라 코드 정렬이 성능에 미치는 영향 을 고려해야 할 수 있습니다 . 이렇게하려면 테스트를 여러 번 실행 한 외부 래퍼가 필요할 수 있습니다 (별도의 앱 도메인 또는 프로세스에서?). 일부 시간은 먼저 "패딩 코드"를 호출하여 JIT 컴파일되도록 강제하여 코드가 다르게 정렬되도록 벤치마킹되었습니다. 완전한 테스트 결과는 다양한 코드 정렬에 대해 최상의 경우와 최악의 경우 타이밍을 제공합니다.


1

벤치 마크 완료에서 가비지 컬렉션 영향을 제거하려는 경우 설정할 가치가 있습니까? GCSettings.LatencyMode 있습니까?

그렇지 않은 경우 생성 된 가비지의 영향이 func벤치 마크의 일부가 되기를 원하면 테스트가 끝날 때 (타이머 내부) 수집을 강제해야하지 않습니까?


0

질문의 기본 문제는 단일 측정으로 모든 질문에 답할 수 있다는 가정입니다. 상황, 특히 C #과 같은 가비지 수집 언어에서 상황을 효과적으로 파악하려면 여러 번 측정해야합니다.

또 다른 대답은 기본 성능을 측정하는 올바른 방법을 제공합니다.

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

그러나이 단일 측정은 가비지 수집을 고려하지 않습니다. 적절한 프로필은 추가로 많은 호출에 걸쳐 분산 된 가비지 수집의 최악의 경우 성능을 설명합니다 (이 숫자는 VM이 ​​남은 가비지 수집없이 종료 될 수 있지만 .NET의 두 가지 구현을 비교하는 데 여전히 유용하므로 쓸모가 func없습니다).

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

또한 한 번만 호출되는 메서드에 대해 가비지 수집의 최악의 성능을 측정 할 수도 있습니다.

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

그러나 프로파일 링 할 특정 가능한 추가 측정을 권장하는 것보다 더 중요한 것은 한 종류의 통계가 아닌 여러 다른 통계를 측정해야한다는 생각입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.