C ++에서 명령문 순서 적용


111

고정 된 순서로 실행하려는 문이 여러 개 있다고 가정합니다. 최적화 수준 2에서 g ++를 사용하여 일부 명령문을 재정렬 할 수 있습니다. 특정 명령문 순서를 적용하려면 어떤 도구가 필요합니까?

다음 예를 고려하십시오.

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

이 예에서는 문 1-3이 주어진 순서로 실행되는 것이 중요합니다. 그러나 컴파일러는 문 2가 1과 3과 독립적이라고 생각하고 다음과 같이 코드를 실행할 수 없습니까?

using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

34
컴파일러가 독립적이지 않은데도 독립적이라고 생각하면 컴파일러가 고장난 것이므로 더 나은 컴파일러를 사용해야합니다.
데이비드 슈워츠


1
__sync_synchronize()도움 이 될 수 있습니까?
vsz

3
@HowardHinnant : 그러한 지시문이 정의되고 앨리어싱 규칙이 이전에 쓰여진 데이터 장벽 이후에 수행 된 읽기를 면제하도록 조정되면 표준 C의 의미 론적 힘이 엄청나게 향상됩니다.
supercat

4
@DavidSchwartz이 경우 foo실행하는 데 걸리는 시간을 측정하는 것 입니다. 다른 스레드의 관찰을 무시할 수있는 것처럼 컴파일러가 재정렬 할 때 무시할 수 있습니다.
CodesInChaos

답변:


100

C ++ 표준위원회와 논의한 후 좀 더 포괄적 인 답변을 제공하고자합니다. C ++위원회의 일원 일뿐만 아니라 LLVM 및 Clang 컴파일러의 개발자이기도합니다.

기본적으로 이러한 변환을 달성하기 위해 순서에서 장벽이나 일부 작업을 사용할 방법이 없습니다. 근본적인 문제는 정수 덧셈과 같은 동작 의미가 구현에 완전히 알려져 있다는 것입니다. 시뮬레이션 할 수 있고 올바른 프로그램으로 관찰 할 수 없다는 것을 알고 항상 자유롭게 이동할 수 있습니다.

이를 방지하려고 노력할 수는 있지만 매우 부정적인 결과를 낳고 결국 실패 할 것입니다.

첫째, 컴파일러에서이를 방지하는 유일한 방법은 이러한 모든 기본 작업을 관찰 할 수 있음을 알리는 것입니다. 문제는 이것이 컴파일러 최적화의 압도적 인 대부분을 배제한다는 것입니다. 컴파일러 내부에는 타이밍 이 관찰 가능 하다는 것을 모델링 할 수있는 좋은 메커니즘이 기본적으로 없습니다 . 어떤 작업에 시간이 걸리는지에 대한 좋은 모델도 없습니다 . 예를 들어 32 비트 부호없는 정수를 부호없는 64 비트 정수로 변환하는 데 시간이 걸리나요? x86-64에서는 시간이 걸리지 않지만 다른 아키텍처에서는 0이 아닌 시간이 걸립니다. 여기에는 일반적으로 정답이 없습니다.

그러나 컴파일러가 이러한 작업의 순서를 변경하지 못하도록 막는 데 성공하더라도 이것이 충분하다는 보장은 없습니다. x86 머신에서 C ++ 프로그램을 실행하는 유효하고 적합한 방법 인 DynamoRIO를 고려하십시오. 이것은 프로그램의 기계어 코드를 동적으로 평가하는 시스템입니다. 할 수있는 한 가지는 온라인 최적화이며, 타이밍 밖에서 전체 범위의 기본 산술 명령을 추측 적으로 실행할 수도 있습니다. 그리고이 동작은 동적 평가자에게 고유하지 않습니다. 실제 x86 CPU는 명령 (훨씬 적은 수)을 추측하고 동적으로 순서를 변경합니다.

본질적인 깨달음은 (타이밍 수준에서도) 산술을 관찰 할 수 없다는 사실이 컴퓨터의 레이어에 스며든다는 것입니다. 컴파일러, 런타임 및 종종 하드웨어에도 해당됩니다. 관찰 가능하도록 강제하는 것은 컴파일러를 극적으로 제한하지만 하드웨어도 극적으로 제한합니다.

그러나이 모든 것이 여러분이 희망을 잃게해서는 안됩니다. 기본적인 수학 연산의 실행 시간을 정하고 싶을 때 안정적으로 작동하는 기술을 잘 연구했습니다. 일반적으로 마이크로 벤치마킹을 할 때 사용됩니다 . CppCon2015에서 이에 대해 이야기했습니다 : https://youtu.be/nXaxk27zwlk

여기에 표시된 기술은 Google과 같은 다양한 마이크로 벤치 마크 라이브러리에서도 제공됩니다. https://github.com/google/benchmark#preventing-optimization

이러한 기술의 핵심은 데이터에 집중하는 것입니다. 옵티 마이저에 대해 계산에 대한 입력을 불투명하게 만들고 옵티 마이저에 대해 계산 결과를 불투명하게 만듭니다. 이 작업을 마치면 안정적으로 시간을 측정 할 수 있습니다. 원래 질문에서 실제 버전의 예제를 살펴 보되 foo구현 에 완전히 표시 되는 정의 를 사용합니다. 또한 DoNotOptimize여기에서 찾을 수있는 Google Benchmark 라이브러리에서 (비 휴대용) 버전을 추출했습니다 . https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208

#include <chrono>

template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
  asm volatile("" : "+m"(const_cast<T &>(value)));
}

// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto input = 42;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize(input);
  auto output = foo(input);       // Statement 2
  DoNotOptimize(output);
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

여기서는 입력 데이터와 출력 데이터가 계산 주변에서 최적화 불가능한 것으로 표시되고 foo해당 마커 주변에서만 계산 된 타이밍이 있는지 확인합니다 . 데이터를 사용하여 계산을 좁히기 때문에 두 타이밍 사이에 머무르는 것이 보장되지만 계산 자체는 최적화 될 수 있습니다. Clang / LLVM의 최근 빌드에서 생성 된 결과 x86-64 어셈블리는 다음과 같습니다.

% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
        .text
        .file   "so.cpp"
        .globl  _Z8time_foov
        .p2align        4, 0x90
        .type   _Z8time_foov,@function
_Z8time_foov:                           # @_Z8time_foov
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbx
.Ltmp0:
        .cfi_def_cfa_offset 16
        subq    $16, %rsp
.Ltmp1:
        .cfi_def_cfa_offset 32
.Ltmp2:
        .cfi_offset %rbx, -16
        movl    $42, 8(%rsp)
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, %rbx
        #APP
        #NO_APP
        movl    8(%rsp), %eax
        addl    %eax, %eax              # This is "foo"!
        movl    %eax, 12(%rsp)
        #APP
        #NO_APP
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        subq    %rbx, %rax
        addq    $16, %rsp
        popq    %rbx
        retq
.Lfunc_end0:
        .size   _Z8time_foov, .Lfunc_end0-_Z8time_foov
        .cfi_endproc


        .ident  "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
        .section        ".note.GNU-stack","",@progbits

여기에서 컴파일러가 호출을 foo(input)단일 명령어로 최적화하는 것을 볼 수 addl %eax, %eax있지만, 타이밍 외부로 이동하거나 상수 입력에도 불구하고 완전히 제거하지 않습니다.

이것이 도움이되기를 바라며, C ++ 표준위원회는 DoNotOptimize여기 와 유사한 API 표준화 가능성을 검토하고 있습니다.


1
답변 주셔서 감사합니다. 새로운 베스트 답변으로 표시했습니다. 이 작업을 더 일찍 할 수도 있었지만 몇 달 동안이 스택 오버플로 페이지를 읽지 않았습니다. Clang 컴파일러를 사용하여 C ++ 프로그램을 만드는 데 매우 관심이 있습니다. 무엇보다도 Clang의 변수 이름에 유니 코드 문자를 사용할 수 있다는 점이 마음에 듭니다. Stackoverflow에서 Clang에 대해 더 많은 질문을 할 것 같습니다.
S2108887

5
이것이 foo가 완전히 최적화되는 것을 방지하는 방법을 이해하지만 이것이 Clock::now()foo ()와 관련 하여 호출 이 재정렬되는 것을 방지하는 이유를 조금 자세히 설명해 주 시겠습니까? 가정해야 optimzer 하는가 DoNotOptimizeClock::now()에 액세스 할 수 있으며 차례로 인 - 출력에 넥타이 것 몇 가지 일반적인 전역 상태를 수정할 수 있는가? 아니면 최적화 프로그램 구현의 현재 제한 사항에 의존하고 있습니까?
MikeMB 2017

2
DoNotOptimize이 예에서는 종합적으로 "관찰 가능한"이벤트입니다. 마치 입력의 표현을 사용하여 일부 터미널에 표시되는 출력을 개념적으로 인쇄하는 것과 같습니다. 시계를 읽는 것도 관찰 가능하기 때문에 (시간이 지나가는 것을 관찰하고 있음) 프로그램의 관찰 가능한 동작을 변경하지 않고는 다시 정렬 할 수 없습니다.
챈들러 Carruth

1
"관찰 가능"이라는 개념이 아직 명확하지 않습니다. foo함수가 잠시 차단 될 수있는 소켓에서 읽는 것과 같은 일부 작업을 수행하는 경우 이것이 관측 가능한 작업을 계산합니까? 그리고 read"완전히 알려진"연산이 아니기 때문에 (맞습니까?) 코드가 순서대로 유지됩니까?
ravenisadesk

근본적인 문제는 정수 더하기와 같은 것의 작동 의미가 구현에 완전히 알려져 있다는 것입니다. " 그러나 문제는 정수 덧셈의 의미가 아니라 foo () 함수를 호출하는 의미라고 생각됩니다. foo ()가 동일한 컴파일 단위에 있지 않으면 foo ()와 clock ()이 상호 작용하지 않는다는 것을 어떻게 알 수 있습니까?
Dave

59

요약:

재정렬을 방지 할 수있는 확실한 방법은없는 것 같지만 링크 타임 / 전체 프로그램 최적화가 활성화되지 않은 한 별도의 컴파일 단위에서 호출 된 함수를 찾는 것이 상당히 좋은 방법 인 것 같습니다. . (적어도 GCC에서는 이것이 다른 컴파일러에서도 가능하다는 것을 논리에서 제안 할 수 있습니다.) 이것은 함수 호출 비용으로 발생합니다. 인라인 코드는 정의에 따라 동일한 컴파일 단위에 있으며 재정렬이 가능합니다.

원래 답변 :

GCC는 -O2 최적화에 따라 호출 순서를 변경합니다.

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0 :

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

그러나:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

이제 foo ()를 extern 함수로 사용합니다.

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

그러나 이것이 -flto (링크 시간 최적화)와 연결되어있는 경우 :

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
MSVC와 ICC도 마찬가지입니다. Clang은 원래 시퀀스를 보존하는 유일한 것입니다.
Cody Gray

3
t1과 t2를 어디에도 사용하지 않기 때문에 결과가 폐기 될 수 있다고 생각하고 코드를 재정렬 할 수 있습니다
phuclv

3
@Niall-더 구체적인 것을 제공 할 수는 없지만 내 의견은 근본적인 이유를 암시한다고 생각합니다. 컴파일러는 foo ()가 now ()에 영향을 줄 수 없으며 그 반대도 마찬가지임을 알고 있으며 재정렬도 마찬가지입니다. 외부 범위 기능과 데이터를 포함하는 다양한 실험이이를 확인하는 것 같습니다. 여기에는 정적 foo ()가 파일 범위 변수 N에 의존하는 것이 포함됩니다. N이 정적으로 선언되면 재정렬이 발생하는 반면, 비 정적으로 선언 된 경우 (즉, 다른 컴파일 단위에서 볼 수 있으므로 잠재적으로 다음의 부작용이 발생할 수 있습니다.) now ()) 재정렬과 같은 extern 함수는 발생하지 않습니다.
제레미

3
@ Lưu Vĩnh Phúc : 단, 호출 자체가 제거되지는 않습니다. 다시 한번, 나는 컴파일러는 부작용이있을 수 있습니다 알고하지 않기 때문에이 용의자 -하지만 않습니다 그 부작용 () foo는의 행동에 영향을 미칠 수 없음을 알고있다.
제레미

3
마지막으로 -flto (링크 타임 최적화)를 지정하면 순서가 변경되지 않은 경우에도 순서가 변경됩니다.
Jeremy

20

재정렬은 컴파일러 또는 프로세서에서 수행 할 수 있습니다.

대부분의 컴파일러는 읽기-쓰기 명령어의 재정렬을 방지하기 위해 플랫폼 별 방법을 제공합니다. gcc에서 이것은

asm volatile("" ::: "memory");

( 자세한 내용은 여기 )

이것은 읽기 / 쓰기에 의존하는 한 재정렬 작업을 간접적으로 만 방지합니다.

실제로 나는 시스템 호출이 Clock::now()그러한 장벽과 동일한 효과를 갖는 시스템을 아직 보지 못했습니다 . 결과 어셈블리를 검사하여 확인할 수 있습니다.

그러나 테스트중인 함수가 컴파일 시간 동안 평가되는 것은 드문 일이 아닙니다. "현실적인"실행을 적용하려면 foo()I / O 또는 volatile읽기 에서 입력을 유도해야 할 수 있습니다 .


또 다른 옵션은 inlining을 비활성화하는 것입니다. foo()다시 말하지만 이것은 컴파일러에 따라 다르며 일반적으로 이식성이 없지만 동일한 효과를 갖습니다.

gcc에서 이것은 __attribute__ ((noinline))


@Ruslan은 근본적인 문제를 제기합니다.이 측정이 얼마나 현실적입니까?

실행 시간은 여러 요인에 의해 영향을받습니다. 하나는 실행중인 실제 하드웨어이고 다른 하나는 캐시, 메모리, 디스크 및 CPU 코어와 같은 공유 리소스에 대한 동시 액세스입니다.

따라서 비슷한 타이밍 을 얻기 위해 일반적으로 수행하는 작업 은 낮은 오류 마진으로 재현 가능한지 확인하는 것 입니다. 이것은 그것들을 다소 인공적으로 만듭니다.

"핫 캐시"와 "콜드 캐시"실행 성능은 몇 배 정도 쉽게 다를 수 있지만 실제로는 중간 정도의 성능이 될 것입니다 ( "미온"?).


2
당신의 해킹 asm은 타이머 호출 사이의 명령문 실행 시간에 영향 을 미칩니다. 메모리 클로버 이후의 코드는 메모리에서 모든 변수를 다시로드해야합니다.
루슬란

@Ruslan : 그들의 해킹, 내 것이 아닙니다. 다양한 수준의 제거가 있으며 재현 가능한 결과를 얻으려면 이와 같은 작업을 피할 수 없습니다.
peterchen

2
'asm'을 사용한 해킹은 메모리를 터치하는 작업에 대한 장벽으로 만 도움이되며 OP는 그 이상에 관심이 있습니다. 자세한 내용은 내 대답을 참조하십시오.
Chandler Carruth 2016 년

11

C ++ 언어는 다양한 방식으로 관찰 가능한 것을 정의합니다.

foo()관찰 할 수있는 것이 없으면 완전히 제거 할 수 있습니다. 경우 foo()"로컬"상태에서 저장 값 (스택 또는 개체 어딘가에서 그것을 할)하는 계산을 수행 만 하고 컴파일러가 더 안전하게 파생 포인터가 들어갈 수 증명할 수있는 Clock::now()코드, 다음 관측 결과에 대한이 없습니다 Clock::now()통화 이동 .

경우 foo()파일이나 디스플레이, 컴파일러 상호 작용 즉 증명할 수 Clock::now()않습니다 하지 다음 파일 또는 디스플레이와의 상호 작용이 관찰 행동이기 때문에, 할 수없는 재정렬, 파일 또는 디스플레이와 상호 작용합니다.

컴파일러 고유의 해킹을 사용하여 코드가 이동하지 않도록 할 수 있지만 (인라인 어셈블리처럼) 또 다른 접근 방식은 컴파일러를 능가하는 것입니다.

동적으로로드 된 라이브러리를 만듭니다. 문제의 코드 이전에로드하십시오.

이 라이브러리는 한 가지를 노출합니다.

namespace details {
  void execute( void(*)(void*), void *);
}

다음과 같이 포장합니다.

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

nullary 람다를 압축하고 동적 라이브러리를 사용하여 컴파일러가 이해할 수없는 컨텍스트에서 실행합니다.

동적 라이브러리 내에서 다음을 수행합니다.

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

매우 간단합니다.

이제에 대한 호출 순서를 변경하려면 execute테스트 코드를 컴파일하는 동안에는 불가능한 동적 라이브러리를 이해해야합니다.

foo()부작용이 전혀없이 s를 제거 할 수 있지만 일부는 이기고 일부는 잃습니다.


19
"또 다른 접근 방식은 컴파일러를 능가하는 것입니다." 그 문구가 토끼 구멍을 뚫었다는 신호가 아니라면 그게 뭔지 모르겠습니다. :-)
Cody Gray

1
코드 블록을 실행하는 데 필요한 시간은 컴파일러가 유지 관리하는 데 필요한 "관찰 가능한"동작으로 간주되지 않는다는 점에 유의하는 것이 도움이 될 수 있습니다 . 코드 블록을 실행하는 시간이 "관찰 가능"한 경우 성능 최적화 형태는 허용되지 않습니다. C와 C ++가 "인과성 장벽"을 정의하는 것은 도움이되지만, 장벽 이전의 모든 부작용이 생성 된 코드에 의해 처리 될 때까지 컴파일러가 장벽 이후의 코드 실행을 보류해야하는 "인과성 장벽" 데이터가 완전히 ...
supercat

1
... 하드웨어 캐시를 통해 전파되는 경우 하드웨어 관련 수단을 사용해야하지만 게시 된 모든 쓰기가 완료 될 때까지 기다리는 하드웨어 관련 수단은 모든 보류중인 쓰기가 컴파일러에 의해 추적되도록하는 장벽 없이는 쓸모가 없습니다. 게시 된 모든 쓰기가 완료되었는지 확인하기 위해 하드웨어가 하드웨어에 게시되어야합니다.] 더미 volatile액세스를 사용하거나 외부 코드를 호출 하지 않고는 어느 언어로도이를 수행 할 수있는 방법이 없습니다 .
supercat

4

아니요. C ++ 표준 [intro.execution]에 따르면 :

14 full-expression과 관련된 모든 값 계산 및 부작용은 평가할 다음 full-expression과 관련된 모든 값 계산 및 부작용 전에 시퀀싱됩니다.

전체 표현식은 기본적으로 세미콜론으로 끝나는 명령문입니다. 보시다시피 위의 규칙은 명령문이 순서대로 실행되어야한다고 규정하고 있습니다. 그것은이다 내에서 컴파일러가 더없이 자기 허용되는 문 (즉, 그 이외의 주문에 문을 구성하는 식을 평가하도록 허용 어떤 상황 아래 왼쪽에서 오른쪽으로 또는 어떤 다른 특정).

적용 할 as-if 규칙에 대한 조건이 여기에서 충족되지 않습니다. 어떤 컴파일러가 시스템 시간을 얻기 위해 호출을 재정렬 하는 것이 관찰 가능한 프로그램 동작에 영향을 미치지 않는다는 것을 증명할 수 있다고 생각하는 것은 비합리적 입니다. 관찰 된 동작을 변경하지 않고 시간을 얻기위한 두 번의 호출을 재정렬 할 수있는 상황이 있었다면이를 확실하게 추론 할 수있을만큼 충분히 이해하고 프로그램을 분석하는 컴파일러를 실제로 생성하는 것은 매우 비효율적 일 것입니다.


12
여전히 같은-경우 규칙 불구하고있다
MM

18
으로 AS-경우 규칙 이 관찰 동작을 변경하지 않는 한 컴파일러는 한 코드로 모든 작업을 수행 할 수 있습니다. 실행 시간은 관찰 할 수 없습니다. 결과는 동일한 것으로 (대부분의 컴파일러는 현명한 일이 아니라 재주문 시간 통화를 할 수 있지만, 필요하지 않은) 길이로 코드의 arbutrary 라인을 재정렬 할 수 있도록
Revolver_Ocelot

6
실행 시간은 관찰 할 수 없습니다. 이것은 아주 이상합니다. 실용적이고 비 기술적 인 관점에서 실행 시간 (일명 "성능")은 매우 관찰 가능합니다.
프레데릭 Hamidi

3
시간을 측정하는 방법에 따라 다릅니다. 표준 C ++에서 일부 코드 본문을 실행하는 데 걸린 클럭 사이클 수를 측정하는 것은 불가능합니다.
Peter

3
@dba 당신은 몇 가지를 함께 혼합하고 있습니다. 링커는 더 이상 Win16 응용 프로그램을 생성 할 수 없습니다. 이는 충분히 사실이지만 해당 유형의 이진 생성에 대한 지원을 제거했기 때문입니다. WIn16 앱은 PE 형식을 사용하지 않습니다. 그렇다고 컴파일러 나 링커가 API 함수에 대한 특별한 지식을 갖고 있다는 의미는 아닙니다. 다른 문제는 런타임 라이브러리와 관련이 있습니다. NT 4에서 실행되는 바이너리를 생성하기 위해 최신 버전의 MSVC를 얻는 데 전혀 문제가 없습니다. 사용할 수없는 함수를 호출하는 CRT에서 링크를 시도하자마자 문제가 발생합니다.
Cody Gray

2

아니.

때로는 "as-if"규칙에 따라 문이 다시 정렬 될 수 있습니다. 이것은 논리적으로 서로 독립적이기 때문이 아니라 이러한 독립성을 통해 프로그램의 의미를 변경하지 않고도 이러한 재정렬이 발생할 수 있기 때문입니다.

현재 시간을 가져 오는 시스템 호출을 이동하는 것은 분명히 해당 조건을 충족하지 않습니다. 고의로 또는 무의식적으로 그렇게하는 컴파일러는 규정을 준수하지 않고 정말 어리 석습니다.

일반적으로, 공격적으로 최적화하는 컴파일러조차도 시스템 호출을 "두 번째 추측"하는 표현을 기대하지 않습니다. 시스템 호출이 수행하는 작업에 대해 충분히 알지 못합니다.


5
나는 그것이 어리석은 일이라는 데 동의하지만 부적합 이라고 부르지 않을 것 입니다. 컴파일러는 구체적인 시스템에 대한 시스템 호출이 정확히 무엇을하는지 그리고 부작용이 있는지 알 수 있습니다. 나는 컴파일러가 일반적인 사용 사례를 다루기 위해 그러한 호출을 재정렬하지 않기를 기대하며 표준이 금지하기 때문에 더 나은 사용자 경험을 허용합니다.
Revolver_Ocelot

4
@Revolver_Ocelot : 프로그램의 의미를 변경하는 최적화 (예, 복사 제거를 위해 저장)는 동의 여부에 관계없이 표준을 준수하지 않습니다.
밝기 경주 궤도에

6
사소한 경우 에는 코드가의 상태와 상호 작용 하는 정의 된 방법 int x = 0; clock(); x = y*2; clock();없습니다 . C ++ 표준에서는 스택을 검사 할 수 있고 계산이 발생하는시기를 알 수 있지만 C ++의 문제는 아닙니다 . clock()xclock()
Yakk-Adam Nevraumont

5
Yakk의 요점을 더 자세히 설명하면 첫 번째 결과가에 할당되고 t2두 번째 결과가에 할당되도록 시스템 호출을 다시 정렬 t1하면 해당 값을 사용하면 부적합하고 어리석은 것이 사실입니다.이 답변이 놓친 것은 다음과 같습니다. 준수 컴파일러는 때때로 시스템 호출을 통해 다른 코드를 재정렬 할 수 있습니다. 이 경우, 그것이 무엇을하는지 foo()(예를 들어 그것을 인라인했기 때문에) 알고 , 따라서 (느슨하게 말하면) 순수한 함수이고 주위를 움직일 수 있다면.
Steve Jessop

1
.. 다시 느슨하게 말하면 이것은 실제 구현 (추상 ​​기계는 아니지만)이 y*y시스템 호출 전에 추측 적으로 계산하지 않을 것이라는 보장이 없기 때문입니다 . 또한 실제 구현이 나중에 x사용되는 지점에 관계없이이 추측 계산의 결과를 사용하지 않을 것이라는 보장도 없으므로 clock(). 인라인 된 함수 foo가 어떤 일 을하더라도 부작용이없고에 의해 변경 될 수있는 상태에 의존 할 수없는 경우에도 마찬가지입니다 clock().
Steve Jessop

0

noinline 함수 + 인라인 어셈블리 블랙 박스 + 전체 데이터 종속성

이것은 https://stackoverflow.com/a/38025837/895245를 기반으로 하지만 왜 ::now()거기에서 재정렬 할 수 없는지에 대한 명확한 정당성을 보지 못했기 때문에 오히려 편집증이되고 그것을 noinline 함수 안에 넣습니다. asm.

이렇게하면 재정렬이 일어날 수 없다고 확신합니다. noinline 하면 the ::now와 데이터 종속성 "연결" .

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub 업스트림 .

컴파일 및 실행 :

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

이 방법의 유일한 단점은 하나의 추가 callq명령을 inline메서드에 추가한다는 것 입니다. objdump -CD다음을 main포함하는 쇼 :

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

그래서 우리는 그것이 foo인라인되었지만 get_clock그렇지 않은 것을 보았습니다 .

get_clock 그러나 그 자체는 매우 효율적이며 스택을 건드리지 않는 단일 리프 호출 최적화 명령으로 구성됩니다.

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

클럭 정밀도 자체가 제한되어 있기 때문에 하나의 추가 타이밍 효과를 눈치 채지 못할 것 같습니다 jmpq. 다음에 call관계없이 하나 가 필요합니다.::now()공유 라이브러리에 합니다.

요구 ::now()데이터 종속성이있는 인라인 어셈블리에서

이것은 가능한 가장 효율적인 솔루션이 될 것입니다. jmpq 위에서 언급 한 입니다.

불행히도 다음과 같이 올바르게 수행하기가 매우 어렵습니다. 확장 인라인 ASM에서 printf 호출

그러나 호출없이 인라인 어셈블리에서 직접 시간 측정을 수행 할 수있는 경우이 기술을 사용할 수 있습니다. 예를 들어 gem5 매직 인스 트루먼 테이션 명령 , x86 RDTSC (더 이상 대표적 일지 확실하지 않음) 및 기타 성능 카운터의 경우입니다.

관련 스레드 :

GCC 8.3.0, Ubuntu 19.04로 테스트되었습니다.


1
일반적으로를 "+m"사용하여 스필 / 리로드를 강제 할 필요가 없습니다. "+r"를 사용하면 컴파일러가 값을 구체화 한 다음 변수가 변경되었다고 가정하는 훨씬 더 효율적인 방법입니다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.