2016-08-18 5 views
10

다형성을 사용하여 유형을 전환하는 두 가지 솔루션과 스위치 케이스를 사용하여 실행할 기능 중 하나를 선택하는 두 가지 솔루션의 성능을 테스트해야합니다. I 정말이 코드를 최적화해야합니다. 나는 다음과 같은 테스트 케이스를 작성했다. 코드를 붙여 넣기 만하면된다. g++ -std=c++14 -O3으로 컴파일하고 echo 1 | ./a.out으로 실행해라!) 코드를 읽으면 정말 간단하다.최적화 된 빌드와 스위치 케이스 및 다형성 비교

#include <iostream> 
#include <chrono> 
#include <functional> 
#include <array> 
#include <cassert> 
#include <vector> 
#include <memory> 
using namespace std; 

struct profiler 
{ 
    std::string name; 
    std::chrono::high_resolution_clock::time_point p; 
    profiler(std::string const &n) : 
     name(n), p(std::chrono::high_resolution_clock::now()) { } 
    ~profiler() 
    { 
     using dura = std::chrono::duration<double>; 
     auto d = std::chrono::high_resolution_clock::now() - p; 
     std::cout << name << ": " 
      << std::chrono::duration_cast<dura>(d).count() 
      << std::endl; 
    } 
}; 
#define PROFILE_BLOCK(pbn) profiler _pfinstance(pbn) 

class Base { 
public: 
    virtual int increment(int in) { 
     return in + 2; 
    } 
}; 
class Derived : public Base { 
public: 
    int increment(int in) override { 
     return ++in; 
    } 
}; 

int increment_one(int in) { 
    return in + 2; 
} 
int increment_two(int in) { 
    return ++in; 
} 
int increment_three(int in) { 
    return in + 4; 
} 
int increment_four(int in) { 
    return in + 2; 
} 

static constexpr unsigned long long NUMBER_LOOP{5000000000}; 
int main() { 

    int which_function; 
    cin >> which_function; 

    { 
     PROFILE_BLOCK("nothing"); 
    } 

    { 
     PROFILE_BLOCK("switch case"); 
     auto counter = 0; 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      switch(which_function) { 
      case 0: 
       counter = increment_one(counter); 
       break; 
      case 1: 
       counter = increment_two(counter); 
       break; 
      case 2: 
       counter = increment_three(counter); 
       break; 
      case 3: 
       counter = increment_four(counter); 
       break; 
      default: 
       assert(false); 
       break; 
      } 
     } 
     cout << counter << endl; 
    } 

    { 
     PROFILE_BLOCK("polymorphism"); 
     auto counter = 0; 
     std::unique_ptr<Base> ptr_base{new Derived()}; 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      counter = ptr_base->increment(counter); 
     } 
    } 

    return 0; 
} 

내가 g++ -std=c++14 -O3으로 구축하고 echo 1 | ./a.out 실행할 때 얻을 출력은

nothing: 1.167e-06 
705032704 
switch case: 4.089e-06 
polymorphism: 9.299 

내가 정확히 거의 빠른 속도로 스위치의 경우 원인을 이해하는 데 실패하고있는 nothing 경우와 . 인라인 때문에 이것입니까? 컴파일러가 각 입력 시나리오의 값을 미리 계산하고 조회 테이블에 넣기 때문입니까? 스위치 케이스가 왜 그렇게 빠른가요?

그리고이 시나리오에 대한보다 공정한 성능 테스트를 작성하려면 어떻게해야합니까? 일반적으로 C++ 코드와 어셈블리 사이의 직접적인 최적화되지 않은 변환 또는 컴파일러가 값을 미리 계산하고 컴파일을 건너 뛰고 "no-op 스타일"코드를 생성하는지 여부 때문에 코드가 빠르다는 것을 전혀 이해하지 못합니다.profiler 구조체가 다른 SO 대답 떨어져 바로 복사되었습니다

참고는 바와 같이 @ 아래 코멘트에 지적 시간

주를 측정한다는 사실 이외의 질문에 그 관련이 없습니다 dau_sama는 clunk 대신 gcc를 사용하여 동일한 테스트를 실행하여 스위치 케이스가 훨씬 길어졌지만 (이 경우에는 3.34) 여전히 다형성 케이스보다 훨씬 적습니다.

+1

스위치 케이스 경로 또는 다른 실행에 사용하는 단 조건이다. 최신 CPU는 파이프 라인 및 분기 예측에 매우 뛰어납니다. –

+0

내가 이해하지 못한 것은 컴파일러가 분기 '5000000000' 번을 실행하는지 또는 사전 계산 결과를 입력에 기반한 조회 테이블에 넣고 값을 대체하는지 여부뿐입니다. 분기 '5000000000' 번에는 아직 아무 것도하지 않는 것과 같은 순서의 ** ** ** 시간이 걸릴 것입니다. – Curious

+3

이상한 결과가 나옵니다. 아무것도 : 4.6e-08 스위치 케이스 : 3.781 polymorphism : 1.57663 –

답변

0

컴파일러 최적화의 복잡한 세부 사항에 대해 많이 알지는 않지만 컴파일러가 스위치 및 루프의 순서를 변경하여 결과가

인 코드의 결과가 플랫폼에 큰 차이가 있다고 생각할 수 있습니다.

switch(which_function) { 
    case 1: count += 2*NUMBER_LOOP; break; 
    case 2: count += NUMBER_LOOP; break; 
    ... 
} 

이것은 매우 간단한 코드이며, 플랫폼의 스위치 케이스의 낮은 실행 시간을 설명 할 수있다 :

switch(which_function) { 
    case 1: // execute increment_one NUMBER_LOOP times 
    case 2: // execute increment_two NUMBER_LOOP times 
    ... 
} 

이 증가 기능을 간단 주어진, 컴파일러는 그 함수를 인라인 할 수있다. 주석에 명시된대로 count 변수를 volatile으로 설정하면이 최적화가 비활성화됩니다.

물론이 문제를 조사하는 유일한 방법은 컴파일 된 이진 파일을 보는 것입니다.

1

나는 다른 결과를 얻습니다 :
1). 최적화하지 않고

$ g++ -std=c++11 -O0 perf.cpp 
$ ./a.out 
2 
nothing: 1.761e-06 
18446744072234715136 
switch case: 25.1785 
polymorphism: 110.119 

이 결과는 정상입니다. 가상 함수 호출에는 가상 함수 테이블에 대한 검색 작업이 있어야하지만 비 가상 함수 호출에는이 검색 단계가 없습니다.

2).O3 최적화가 적용된

$g++ -std=c++11 -O3 perf.cpp 
$ ./a.out 
2 
nothing: 1.44e-07 
18446744072234715136 
switch case: 8.4832 
polymorphism: 3.34942 

ok이 결과는 놀랍지 만 정상적인 현상입니다.
클래스 선언에 정의 된 함수가 인라인되며 컴파일러가 컴파일시 가상 함수 주소를 가져올 수 있습니다.

자세한 내용을 알고 싶다면 어셈블 링 코드를 읽거나, clang을 사용할 수 있으며, 어셈블 코드보다 훨씬 더 읽기 쉬운 IR 코드를 읽을 수 있습니다. 단순히 코드가 제거 관련없는 코드 :

class Base { 
public: 
     virtual int increment(int in) { 
     return in + 2; 
     } 
}; 

class Derived : public Base { 
public: 
    int increment(int in) override { 
     return ++in; 
    } 
}; 

int increment_two(int in) { 
    return ++in; 
} 

int main() { 
    int which_function = 2; 
    int NUMBER_LOOP = 1; 
    Base* ptr_base{new Derived()}; 

    for (long i = 0; i < NUMBER_LOOP; ++i) { 
      switch(which_function) { 
      case 2: 
       increment_two(1); 
       break; 
      } 
    } 

    for (long i = 0; i < NUMBER_LOOP; ++i) { 
     ptr_base->increment(1); 
    } 

    return 0; 
} 

$ g ++ -std = C++ 11 -O0 = 3 code.cpp는 code.s

using clang: 
$ clang -std=c++11 -O3 -S -emit-llvm code.cpp 
here post the clang IR code: 

; ModuleID = 'xp.cpp' 
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" 
target triple = "x86_64-unknown-linux-gnu" 

; Function Attrs: norecurse nounwind readnone uwtable 
define i32 @_Z13increment_twoi(i32 %in) #0 { 
entry: 
    %inc = add nsw i32 %in, 1 
    ret i32 %inc 
} 

; Function Attrs: norecurse uwtable 
define i32 @main() #1 { 
entry: 
    ret i32 0 
} 

attributes #0 = { norecurse nounwind readnone uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" } 
attributes #1 = { norecurse uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" } 

!llvm.ident = !{!0} 

!0 = !{!"clang version 3.8.0 (tags/RELEASE_380/final)"} 
6

을 읽을 수
-S 코드의 문제점은 이와 같이 벤치 마크를 수행 할 때 의미있는 결과를 얻으려면 단순히 for 루프와 많은 수를 사용할 수 없다는 것입니다. -O3 최적화로 컴파일 할 때 컴파일러는 루프에서 계산을 끌어 올리거나 루프 풀기 등을 수행 할 수 있으며 컴파일시에서 이 결과를 계산하여 이진 코드로 하드 ​​코딩합니다. "있는 그대로"규칙에 따라 차이점을 알 수 없습니다. 이렇게하면 아주 작은 코드를 벤치마킹하는 것이 어렵지만 가능한 빨리 코드를 작성하는 것이 옵티마이 저 작업입니다. 옵티마이 저가 똑같은 일을 계속 반복하고 있음을 볼 수 있다면, 모든 계산을 함께 접어 벤치 마크 메커니즘을 무력화시킬 수 있습니다. 컴파일러는 루프를 풀다 또는 기타의 독립 실행 있어야 무엇에서 을 분석하는 시도를 두려워되도록

는 문제를 해결하려면, 당신은 기본적으로 벤치 마크 루프 벤치 마크 프레임 워크의 특정 부분을 당황하게 할 필요가 테스트중인 코드.

코드를 수정 한 버전에서 Google 벤치 마크 라이브러리의 2 비트 코드를 사용했습니다. 여기에 무슨 일이 일어나고 있는지 이해하는 가장 좋은 방법은, 간단히 말해서 https://www.youtube.com/watch?v=nXaxk27zwlk

CppNow 2015 년에 있었다 챈들러 Carruth의 뛰어난 이야기를보고, 무엇을 추가하는 것은 두 개의 인라인 어셈블리 지침, "DoNotOptimize"와 "ClobberMemory"입니다. 이러한 어셈블리는 빈 블록이며 컴파일 된 코드에는 실제 지침이 없지만 asm volatile으로 표시됩니다. 이는 옵티마이 저가 알 수없는 부작용이 있고 어셈블리 자체를 분석하지 말아야 함을 알립니다. "memory" 지시어는 잠재적으로 모든 메모리 주소를 읽고 쓸 수 있음을 의미합니다. "DoNotOptimize"으로 표시된 모든 변수는이 어셈블리에 "알려진"것으로 간주되므로이 함수 중 하나를 호출하면 해당 변수가 비어있는 옵티 마이저의 추론에서 효과적으로 "스크램블링"됩니다 , 이러한 함수가 호출 된 후에 값을 알 수없는 방식으로 변경했을 수 있으므로 루프 언 롤링 및 다른 종류의 최적화가 불리해질 것으로 가정해야합니다.

사실
$ g++ -std=c++14 main.cpp 
$ echo 1 |./a.out 
nothing: 3.864e-06 
705032704 
switch case: 20.385 
polymorphism: 91.0152 
$ g++ -std=c++14 -O3 main.cpp 
$ echo 1 |./a.out 
nothing: 6.74e-07 
705032704 
switch case: 4.59485 
polymorphism: 2.5395 

나는이에 의해 꽤 놀랐어요, 나는 그 스위치를 생각 : 나는 그것을 실행할 때

다음
#include <iostream> 
#include <chrono> 
#include <functional> 
#include <array> 
#include <cassert> 
#include <vector> 
#include <memory> 
using namespace std; 

// From google benchmarks framework 
// See also Chandler Carruth's talk on microoptimizations and benchmarking 
// https://www.youtube.com/watch?v=nXaxk27zwlk 
namespace bench { 

#if defined(__GNUC__) 
#define BENCHMARK_ALWAYS_INLINE __attribute__((always_inline)) 
#else 
#define BENCHMARK_ALWAYS_INLINE 
#endif 

template <class Tp> 
inline BENCHMARK_ALWAYS_INLINE void 
DoNotOptimize(Tp const & value) { 
    asm volatile("" : : "g"(value) : "memory"); 
} 

inline BENCHMARK_ALWAYS_INLINE void 
ClobberMemory() { 
    asm volatile("" : : : "memory"); 
} 

} // end namespace bench 

struct profiler 
{ 
    std::string name; 
    std::chrono::high_resolution_clock::time_point p; 
    profiler(std::string const &n) : 
     name(n), p(std::chrono::high_resolution_clock::now()) { } 
    ~profiler() 
    { 
     using dura = std::chrono::duration<double>; 
     auto d = std::chrono::high_resolution_clock::now() - p; 
     std::cout << name << ": " 
      << std::chrono::duration_cast<dura>(d).count() 
      << std::endl; 
    } 
}; 
#define PROFILE_BLOCK(pbn) profiler _pfinstance(pbn) 

class Base { 
public: 
    virtual int increment(int in) { 
     return in + 2; 
    } 
}; 
class Derived : public Base { 
public: 
    int increment(int in) override { 
     return ++in; 
    } 
}; 

int increment_one(int in) { 
    return in + 2; 
} 
int increment_two(int in) { 
    return ++in; 
} 
int increment_three(int in) { 
    return in + 4; 
} 
int increment_four(int in) { 
    return in + 2; 
} 

static constexpr unsigned long long NUMBER_LOOP{5000000000}; 
int main() { 

    int which_function; 
    cin >> which_function; 

    { 
     PROFILE_BLOCK("nothing"); 
    } 

    { 
     PROFILE_BLOCK("switch case"); 
     auto counter = 0; 
     bench::DoNotOptimize(counter); 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      bench::DoNotOptimize(i); 
      switch(which_function) { 
      case 0: 
       counter = increment_one(counter); 
       break; 
      case 1: 
       counter = increment_two(counter); 
       break; 
      case 2: 
       counter = increment_three(counter); 
       break; 
      case 3: 
       counter = increment_four(counter); 
       break; 
      default: 
       assert(false); 
       break; 
      } 
      bench::ClobberMemory(); 
     } 
     cout << counter << endl; 
    } 

    { 
     PROFILE_BLOCK("polymorphism"); 
     auto counter = 0; 
     bench::DoNotOptimize(counter); 
     std::unique_ptr<Base> ptr_base{new Derived()}; 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      bench::DoNotOptimize(i); 
      counter = ptr_base->increment(counter); 
      bench::ClobberMemory(); 
     } 
    } 

    return 0; 
} 

내가 무엇을 얻을 :

는 다음 코드와 ouptut 내 수정 된 버전입니다 사건은 항상 더 빨라야합니다. 어쩌면 난독 화 지침을 조정해야 할 수도 있고, 아니면 내가 틀 렸을 수도 있습니다.

차이점을 이해하기 위해 생성 된 어셈블리를 살펴볼 수 있습니다. 챈들러가하는 것처럼 perf을 사용하거나, godbolt와 같은 것을 사용할 수 있습니다.

godbolt gcc of your code에 대한 링크가 있습니다. 나는 모두를 읽지 못했지만, 나에게 눈에 띄는 한 가지가이 절에 있다는 것입니다 : ja .L16, je .L17, jne .L18 :

 pushq %r13 
     pushq %r12 
     leaq 16(%rdi), %r12 
     pushq %rbp 
     pushq %rbx 
     subq $24, %rsp 
     testq %rsi, %rsi 
     movq %r12, (%rdi) 
     je  .L5 
     movq %rdi, %rbx 
     movq %rsi, %rdi 
     movq %rsi, %r13 
     call strlen 
     cmpq $15, %rax 
     movq %rax, %rbp 
     movq %rax, 8(%rsp) 
     ja  .L16 
     cmpq $1, %rax 
     je  .L17 
     testq %rax, %rax 
     jne  .L18 
.L9: 
     movq 8(%rsp), %rax 
     movq (%rbx), %rdx 
     movq %rax, 8(%rbx) 
     movb $0, (%rdx,%rax) 
     addq $24, %rsp 
     popq %rbx 
     popq %rbp 
     popq %r12 
     popq %r13 
     ret 
.L16: 
     leaq 8(%rsp), %rsi 
     xorl %edx, %edx 
     movq %rbx, %rdi 
     call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long) 
     movq 8(%rsp), %rdx 
     movq %rax, (%rbx) 
     movq %rax, %rdi 
     movq %rdx, 16(%rbx) 
.L7: 
     movq %rbp, %rdx 
     movq %r13, %rsi 
     call memcpy 
     jmp  .L9 
.L17: 
     movzbl 0(%r13), %eax 
     movb %al, 16(%rbx) 
     jmp  .L9 
.L5: 
     movl $.LC3, %edi 
     call std::__throw_logic_error(char const*) 
.L18: 

당신이 연속 점프 지침이있다. 제 생각에는 아마 switch 성명서라고 생각합니다. 그러나 이러한 진술이 어디로 돌아 왔는지 살펴볼 때 모두는 문으로 되돌아 가지 않는 .L9로 다시 돌아갑니다. 그래서 용의자 옵티마이 저는 루프 외부에 switch을 올려 놓으면 루프의 출력 결과를 각 가능한 입력에 대해 쉽게 계산할 수 있고 벤치 마크를 제로 시간에 실행하는 것처럼 보입니다. 한편

, 내 버전의 생성 된 어셈블리를 볼 때, 그것은 여전히 ​​그 같은 .L16, .L17.L18 점프 .L9에 그들은 모두 점프가 있습니다. 그래서 ... 나는 그것이 무엇을 의미하는지 정확히 모릅니다. 그러나 잘하면 당신이 그것을 이해하는 데 도움이됩니다.


편집 :

은 @Holt에 의해 만들어진 코멘트에 위로 다음, 당신의 네 가지 파생 클래스가되도록 virtual 경우, 더 switch 경우를 일치하도록하는 코드와 추상 기본 클래스를 조정 . 이것은 제가 예상했던 것과 비슷한 결과를줍니다. 내가 줄 수있는 가장 좋은 설명은 아마도 파생 클래스가 하나 밖에 없을 때 컴파일러가 "가상화"또는 뭔가를 수행 할 수 있다는 것입니다. 최신 버전 gcc은 예를 들어 -O3이 전달되면 링크 타임 최적화를 수행합니다.

결과

$ g++ -std=c++14 -O3 main.cpp 
$ echo 1|./a.out 
nothing: 4.92e-07 
705032704 
switch case: 4.56484 
polymorphism: 9.16065 
$ echo 2|./a.out 
nothing: 6.25e-07 
-1474836480 
switch case: 5.31955 
polymorphism: 9.22714 
$ echo 3|./a.out 
nothing: 5.42e-07 
1410065408 
switch case: 3.91608 
polymorphism: 9.17771 

조정 코드 :

#include <iostream> 
#include <chrono> 
#include <functional> 
#include <array> 
#include <cassert> 
#include <vector> 
#include <memory> 
using namespace std; 

// From google benchmarks framework 
// See also Chandler Carruth's talk on microoptimizations and benchmarking 
// https://www.youtube.com/watch?v=nXaxk27zwlk 
namespace bench { 

#if defined(__GNUC__) 
#define BENCHMARK_ALWAYS_INLINE __attribute__((always_inline)) 
#else 
#define BENCHMARK_ALWAYS_INLINE 
#endif 

template <class Tp> 
inline BENCHMARK_ALWAYS_INLINE void 
DoNotOptimize(Tp const & value) { 
    asm volatile("" : : "g"(value) : "memory"); 
} 

inline BENCHMARK_ALWAYS_INLINE void 
ClobberMemory() { 
    asm volatile("" : : : "memory"); 
} 

} // end namespace bench 

struct profiler 
{ 
    std::string name; 
    std::chrono::high_resolution_clock::time_point p; 
    profiler(std::string const &n) : 
     name(n), p(std::chrono::high_resolution_clock::now()) { } 
    ~profiler() 
    { 
     using dura = std::chrono::duration<double>; 
     auto d = std::chrono::high_resolution_clock::now() - p; 
     std::cout << name << ": " 
      << std::chrono::duration_cast<dura>(d).count() 
      << std::endl; 
    } 
}; 
#define PROFILE_BLOCK(pbn) profiler _pfinstance(pbn) 

int increment_one(int in) { 
    return in + 2; 
} 
int increment_two(int in) { 
    return ++in; 
} 
int increment_three(int in) { 
    return in + 4; 
} 
int increment_four(int in) { 
    return in + 2; 
} 

class Base { 
public: 
    virtual int increment(int in) = 0; 
}; 

class Derived1 : public Base { 
public: 
    int increment(int in) override { 
     return increment_one(in); 
    } 
}; 

class Derived2 : public Base { 
public: 
    int increment(int in) override { 
     return increment_two(in); 
    } 
}; 

class Derived3 : public Base { 
public: 
    int increment(int in) override { 
     return increment_three(in); 
    } 
}; 

class Derived4 : public Base { 
public: 
    int increment(int in) override { 
     return increment_four(in); 
    } 
}; 


static constexpr unsigned long long NUMBER_LOOP{5000000000}; 
int main() { 

    int which_function; 
    cin >> which_function; 

    { 
     PROFILE_BLOCK("nothing"); 
    } 

    { 
     PROFILE_BLOCK("switch case"); 
     auto counter = 0; 
     bench::DoNotOptimize(counter); 
     bench::DoNotOptimize(which_function); 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      bench::DoNotOptimize(i); 
      switch(which_function) { 
      case 0: 
       counter = increment_one(counter); 
       break; 
      case 1: 
       counter = increment_two(counter); 
       break; 
      case 2: 
       counter = increment_three(counter); 
       break; 
      case 3: 
       counter = increment_four(counter); 
       break; 
      default: 
       assert(false); 
       break; 
      } 
      bench::ClobberMemory(); 
     } 
     cout << counter << endl; 
    } 

    { 
     PROFILE_BLOCK("polymorphism"); 
     auto counter = 0; 
     bench::DoNotOptimize(counter); 
     std::unique_ptr<Base> ptr_base; 
     switch(which_function) { 
     case 0: 
      ptr_base.reset(new Derived1()); 
      break; 
     case 1: 
      ptr_base.reset(new Derived2()); 
      break; 
     case 2: 
      ptr_base.reset(new Derived3()); 
      break; 
     case 3: 
      ptr_base.reset(new Derived4()); 
      break; 
     default: 
      assert(false); 
      break; 
     } 
     bench::DoNotOptimize(*ptr_base); 
     for (unsigned long long i = 0; i < NUMBER_LOOP; ++i) { 
      bench::DoNotOptimize(i); 
      counter = ptr_base->increment(counter); 
      bench::ClobberMemory(); 
     } 
    } 

    return 0; 
}