문제

<배경>

C++ 코드를 최적화해야 하는 시점에 와 있습니다.분자 시뮬레이션용 라이브러리를 작성 중인데 새 기능을 추가해야 합니다.이전에 이미 이 기능을 추가하려고 시도했지만 그런 다음 중첩 루프에서 호출되는 가상 함수를 사용했습니다.나는 그것에 대해 나쁜 감정을 가지고 있었고 첫 번째 구현은 이것이 나쁜 생각이라는 것을 증명했습니다.그러나 이것은 개념을 테스트하기에는 괜찮았습니다.

< /배경>

이제 이 기능이 최대한 빨라야 합니다(어셈블리 코드나 GPU 계산이 없어도 여전히 C++여야 하고 가독성이 높아야 합니다).이제 나는 템플릿과 클래스 정책(Alexandrescu의 뛰어난 책을 통해)에 대해 조금 더 알게 되었으며 컴파일 타임 코드 생성이 해결책이 될 수 있다고 생각합니다.

하지만 작업을 수행하기 전에 디자인을 테스트해야 합니다. 거대한 이를 라이브러리에 구현하는 작업입니다.문제는 이 새로운 기능의 효율성을 테스트하는 가장 좋은 방법에 관한 것입니다.

분명히 이 g++(및 아마도 다른 컴파일러도)가 없으면 개체 코드에서 일부 불필요한 작업을 유지하기 때문에 최적화를 활성화해야 합니다.또한 1e-3초의 델타가 좋은 디자인과 나쁜 디자인 사이의 차이를 만들 수 있기 때문에 벤치마크에서 새로운 기능을 많이 사용해야 합니다(이 기능은 실제 프로그램에서 백만 번 호출됩니다).

문제는 g++가 최적화하는 동안 때때로 "너무 똑똑"하여 계산 결과가 전혀 사용되지 않는다는 점을 고려하면 전체 루프를 제거할 수 있다는 것입니다.출력 어셈블리 코드를 볼 때 이미 한 번 본 적이 있습니다.

stdout에 일부 인쇄를 추가하면 컴파일러는 루프에서 계산을 수행해야 하지만 아마도 대부분 iostream 구현을 벤치마킹할 것입니다.

그럼 어떻게 해야 할까요? 옳은 라이브러리에서 추출된 작은 기능의 벤치마크?관련 질문:이런 종류의 작업을 수행하는 것이 올바른 접근 방식입니까? 시험관 내에서 작은 단위로 테스트합니까, 아니면 전체 컨텍스트가 필요합니까?

조언을 주셔서 감사합니다!


미세 조정을 허용하는 컴파일러별 옵션부터 다음과 같이 모든 컴파일러에서 작동해야 하는 보다 일반적인 솔루션에 이르기까지 여러 가지 전략이 있는 것 같습니다. volatile 또는 extern.

이 모든 것을 시도해 볼 것 같아요.모든 답변에 감사드립니다!

도움이 되었습니까?

해결책

강제로 하고 싶다면 어느 결과를 삭제하지 않으려면 컴파일러에서 결과를 휘발성 개체에 쓰도록 합니다.해당 작업은 정의에 따라 최적화될 수 없습니다.

template<typename T> void sink(T const& t) {
   volatile T sinkhole = t;
}

iostream 오버헤드가 없으며 생성된 코드에 남아 있어야 하는 복사본만 있습니다.이제 많은 작업에서 결과를 수집하는 경우 하나씩 버리지 않는 것이 가장 좋습니다.이러한 복사본은 여전히 ​​약간의 오버헤드를 추가할 수 있습니다.대신, 어떻게든 단일 비휘발성 개체에 모든 결과를 수집한 다음(따라서 모든 개별 결과가 필요함) 해당 결과 개체를 휘발성 개체에 할당합니다.예:개별 작업이 모두 문자열을 생성하는 경우 모듈로 1<<32로 모든 char 값을 함께 추가하여 강제로 평가할 수 있습니다.이는 오버헤드를 거의 추가하지 않습니다.문자열은 캐시에 있을 가능성이 높습니다.추가 결과는 나중에 휘발성에 할당되므로 각 문자열의 각 문자는 실제로 계산되어야 하며 단축키는 허용되지 않습니다.

다른 팁

당신이 없다면 진짜 공격적인 컴파일러 (발생할 수 있음), 체크섬 계산 (모든 결과를 함께 추가)하고 체크섬을 출력하는 것이 좋습니다.

그 외에는 벤치 마크를 실행하기 전에 생성 된 어셈블리 코드를 살펴볼 수 있으므로 루프가 실제로 실행 중인지 시각적으로 확인할 수 있습니다.

컴파일러는 발생할 수없는 코드 브랜치 만 제거 할 수 있습니다. 지점을 실행해야한다는 것을 배제 할 수없는 한, 그것을 제거하지는 않습니다. 어딘가에 데이터 종속성이있는 한 코드가 있으며 실행됩니다. 컴파일러는 프로그램의 어떤 측면이 실행되지 않을지 추정하고 시도하지 않는 것에 대해 너무 똑똑하지 않습니다. 왜냐하면 그것은 NP 문제이며 거의 계산할 수 없기 때문입니다. 그들은 다음과 같은 간단한 점검을 가지고 있습니다 if (0), 그러나 그것은 그것에 관한 것입니다.

저의 겸손한 의견은 C/C ++가 부울 표현을 평가하는 방식과 같은 이전에 다른 문제에 맞았을 것입니다.

그러나 어쨌든, 이것은 속도 테스트에 관한 것이기 때문에, 당신은 일이 직접 요구되는지 확인할 수 있습니다. 또는 정적 변수가 증가합니다. 테스트가 끝나면 생성 된 숫자를 인쇄하십시오. 결과는 동일합니다.

비트로 내 테스트에 대한 귀하의 질문에 답변하려면 : 그렇습니다. 앱이 시간이 너무 중요하다면 그렇게하십시오. 반면에, 당신의 설명은 다른 문제에 대한 힌트입니다. 델타가 1E-3 초의 기간이라면, 해당 방법을 매우 자주 ( 1e-3 초는 무시할 수 없습니다).

모델링하는 문제 도메인은 매우 복잡하고 데이터 세트는 아마도 거대 할 것입니다. 그런 것들은 항상 흥미로운 노력입니다. 그러나 올바른 데이터 구조와 알고리즘을 먼저 가지고 있는지 확인하고 그 이후로 원하는 모든 것을 마이크로 최적화하십시오. 그래서 나는 먼저 전체 맥락을 살펴 보라고 말하고 싶습니다. ;-)

호기심으로 계산하는 문제는 무엇입니까?

편집에 대한 최적화에 대한 많은 제어가 있습니다. -o1, -o2 등은 많은 스위치에 대한 별칭 일뿐입니다.

남자 페이지에서

       -O2 turns on all optimization flags specified by -O.  It also turns
       on the following optimization flags: -fthread-jumps -falign-func‐
       tions  -falign-jumps -falign-loops  -falign-labels -fcaller-saves
       -fcrossjumping -fcse-follow-jumps  -fcse-skip-blocks
       -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
       -fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove -fre‐
       order-blocks  -freorder-functions -frerun-cse-after-loop
       -fsched-interblock  -fsched-spec -fschedule-insns  -fsched‐
       ule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
       -ftree-vrp

이 명령을 조정하고 사용하여 조사 할 옵션을 좁히는 데 도움이됩니다.

       ...
       Alternatively you can discover which binary optimizations are
       enabled by -O3 by using:

               gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
               gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
               diff /tmp/O2-opts /tmp/O3-opts Φ grep enabled

범인 최적화를 찾으면 Cout이 필요하지 않아야합니다.

이것이 가능하다면 코드를 다음과 같이 분할해 볼 수 있습니다.

  • 모든 최적화를 켠 상태에서 컴파일하여 테스트하려는 라이브러리
  • 최적화가 꺼진 상태에서 라이브러리를 동적으로 연결하는 테스트 프로그램

그렇지 않으면 최적화 속성을 사용하여 테스트 기능에 대해 다른 최적화 수준(gcc를 사용하는 것처럼 보입니다...)을 지정할 수 있습니다(참조: http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes).

별도의 CPP 파일로 더미 함수를 만들 수는 있지만 아무것도하지 않지만 계산 결과의 유형이 무엇이든 인수로 취할 수 있습니다. 그런 다음 계산 결과와 함께 해당 기능을 호출하여 GCC가 중간 코드를 생성하도록 강요 할 수 있으며, 유일한 페널티는 기능을 호출하는 데 드는 비용입니다 (결과를 부르지 않으면 결과를 왜곡하지 않아야합니다. 많은!).

#include <iostream>

// Mark coords as extern.
// Compiler is now NOT allowed to optimise away coords
// This it can not remove the loop where you initialise it.
// This is because the code could be used by another compilation unit
extern double coords[500][3];
double coords[500][3];

int main()
{

//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }


std::cout << "hello world !"<< std::endl;
return 0;
}

편집하다: 가장 쉬운 방법은 기능이 실행 된 후 벤치 마크 외부에서 약간의 가짜 방식으로 데이터를 사용하는 것입니다. 처럼,

StartBenchmarking(); // ie, read a performance counter
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }
StopBenchmarking(); // what comes after this won't go into the timer

// this is just to force the compiler to use coords
double foo;
for (int j = 0 ; j < 500 ; ++j )
{
  foo += coords[j][0] + coords[j][1] + coords[j][2]; 
}
cout << foo;

이 경우 때때로 나를 위해 작동하는 것은 시험 관내 함수 내부 테스트하고 벤치 마크 데이터 세트를 통과합니다. 휘발성 물질 포인터. 이것은 컴파일러에게 그 포인터에게 후속 쓰기를 무너 뜨리지 않아야한다고 말합니다 ( 예를 들어 메모리 매핑 I/O). 그래서,

void test1( volatile double *coords )
{
  //perform a simple initialization of all coordinates:
  for (int i=0; i<1500; i+=3)
  {
    coords[i+0] = 3.23;
    coords[i+1] = 1.345;
    coords[i+2] = 123.998;
  }
}

어떤 이유로 든 아직 알아 내지 못했습니다. 아직 MSVC에서는 항상 작동하지는 않지만 종종 조립 출력을 확인하십시오. 또한 기억하십시오 휘발성 물질 일부 컴파일러 최적화 (컴파일러가 포인터의 내용을 레지스터로 유지하는 것을 금지하고 힘이 프로그램 순서대로 쓰기를하는 것을 금지하므로 데이터의 최종 쓰기에 사용하는 경우에만 신뢰할 수 있습니다.

일반적으로 이와 같은 시험 관내 테스트는 전체 이야기가 아니라는 것을 기억하는 한 매우 유용합니다. 나는 보통 새로운 수학 루틴을 이와 같이 분리하여 테스트하여 일관된 데이터에 대한 알고리즘의 캐시와 파이프 라인 특성 만 빠르게 반복 할 수 있습니다.

이와 같은 테스트 튜브 프로파일 링과 "실제 세계"에서 실행하는 것의 차이는 입력 데이터 세트 (때로는 최상의 경우, 때로는 최악의 경우, 때로는 병리학 적)를 얻는다는 것을 의미합니다. 캐시는 입력 할 때 알려지지 않은 상태에 있습니다. 기능은 버스에 다른 스레드를 두드릴 수 있습니다. 따라서이 기능에 대한 벤치 마크를 실행해야합니다 생체 내 끝났을 때도 마찬가지입니다.

GCC가 비슷한 기능을 가지고 있는지는 모르겠지만 VC ++를 사용하면 사용할 수 있습니다.

#pragma optimize

최적화를 선택적으로 켜거나 끄는 것입니다. GCC에 유사한 기능이있는 경우 전체 최적화로 빌드하여 코드가 호출되도록 필요한 곳에 꺼질 수 있습니다.

원치 않는 최적화의 작은 예 :

#include <vector>
#include <iostream>

using namespace std;

int main()
{
double coords[500][3];

//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }


cout << "hello world !"<< endl;
return 0;
}

코드를 "Double Cords [500] [3]For Loop의 끝까지 댓글을 달면 정확히 동일한 어셈블리 코드를 생성합니다 (G ++ 4.3.2로 시도). 이 예제가 너무 간단하다는 것을 알고 있으며 간단한 "좌표"구조의 STD :: 벡터 로이 동작을 보여줄 수 없었습니다.

그러나이 예는 여전히 일부 최적화가 벤치 마크에 오류를 도입 할 수 있으며 라이브러리에 새 코드를 도입 할 때 이런 종류의 놀라움을 피하고 싶었습니다. 새로운 맥락이 일부 최적화를 방해하고 매우 비효율적 인 라이브러리로 이어질 수 있다고 상상하기 쉽습니다.

또한 가상 함수와 동일하게 적용되어야합니다 (그러나 여기서는 증명하지 않습니다). 정적 링크가 작업을 수행하는 맥락에서 사용됩니다. 나는 괜찮은 컴파일러가 가상 기능에 대한 추가 간접 호출을 제거해야한다고 확신합니다. 이 호출을 루프로 시도하고 가상 기능을 부르는 것이 그리 큰 문제가 아니라고 결론을 내릴 수 있습니다. 그런 다음 컴파일러가 정확한 유형의 포인터 유형이 무엇인지 추측 할 수없고 달리기 시간이 20% 증가 할 수없는 상황에서 수십만 번이라고 부를 것입니다 ...

시작시 파일에서 읽으십시오. 코드에서 (input == "x") cout << result_of_benchmark;

컴파일러는 계산을 제거 할 수 없으며 입력이 "X"가 아닌지 확인하면 iOSTream을 벤치마킹하지 않습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top