는 문제를 해결하려면, 당신은 기본적으로 벤치 마크 루프 벤치 마크 프레임 워크의 특정 부분을 당황하게 할 필요가 테스트중인 코드.
코드를 수정 한 버전에서 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;
}
스위치 케이스 경로 또는 다른 실행에 사용하는 단 조건이다. 최신 CPU는 파이프 라인 및 분기 예측에 매우 뛰어납니다. –
내가 이해하지 못한 것은 컴파일러가 분기 '5000000000' 번을 실행하는지 또는 사전 계산 결과를 입력에 기반한 조회 테이블에 넣고 값을 대체하는지 여부뿐입니다. 분기 '5000000000' 번에는 아직 아무 것도하지 않는 것과 같은 순서의 ** ** ** 시간이 걸릴 것입니다. – Curious
이상한 결과가 나옵니다. 아무것도 : 4.6e-08 스위치 케이스 : 3.781 polymorphism : 1.57663 –