컴파일러 / 최적화 프로그램이 더 빠른 프로그램을 만들 수 있도록하는 코딩 방법 따라 컴파일러는 성숙했습니다. 그들은 흐름

수년 전, C 컴파일러는 특별히 똑똑하지 않았습니다. 해결 방법으로 K & R은 register 키워드를 발명하여 컴파일러에 힌트를 주었으므로이 변수를 내부 레지스터에 유지하는 것이 좋습니다. 그들은 또한 더 나은 코드를 생성하기 위해 3 차 연산자를 만들었습니다.

시간이 지남에 따라 컴파일러는 성숙했습니다. 그들은 흐름 분석을 통해 당신이 할 수있는 것보다 레지스터에 어떤 값을 저장할지에 대해 더 나은 결정을 내릴 수 있다는 점에서 매우 똑똑해졌습니다. register 키워드가 중요하지 않게되었습니다.

FORTRAN은 별칭 문제 로 인해 일부 작업의 경우 C보다 빠를 수 있습니다 . 이론적으로는 신중한 코딩을 통해 최적화 프로그램이 더 빠른 코드를 생성 할 수 있도록이 제한을 피할 수 있습니다.

컴파일러 / 최적화 프로그램이 더 빠른 코드를 생성 할 수있는 코딩 방법은 무엇입니까?

  • 사용하는 플랫폼과 컴파일러를 식별하면 감사하겠습니다.
  • 이 기술이 작동하는 이유는 무엇입니까?
  • 샘플 코드를 권장합니다.

다음은 관련 질문입니다.

[편집] 이 질문은 프로파일 링 및 최적화를위한 전체 프로세스에 관한 것이 아닙니다. 프로그램이 올바로 작성되고, 전체 최적화로 컴파일되고, 테스트되어 프로덕션에 투입되었다고 가정합니다. 최적화 프로그램이 최선의 작업을 수행하지 못하도록하는 코드에 구문이있을 수 있습니다. 이러한 금지 사항을 제거하고 옵티마이 저가 더 빠른 코드를 생성 할 수 있도록 리팩터링하기 위해 무엇을 할 수 있습니까?

[편집] 오프셋 관련 링크



답변

출력 인수가 아닌 지역 변수에 씁니다! 이것은 앨리어싱 속도 저하를 피하는 데 큰 도움이 될 수 있습니다. 예를 들어, 코드가

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

컴파일러는 foo1! = barOut을 알지 못하므로 루프를 통해 매번 foo1을 다시로드해야합니다. 또한 barOut에 대한 쓰기가 완료 될 때까지 foo2 [i]를 읽을 수 없습니다. 제한된 포인터로 엉망이 될 수 있지만 이렇게하는 것이 효과적이며 훨씬 더 명확합니다.

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

어리석게 들리지만 컴파일러는 메모리에서 인수와 겹칠 수 없기 때문에 로컬 변수를 처리하는 것이 훨씬 더 똑똑 할 수 있습니다. 이것은 두려운로드 히트 스토어 (Francis Boivin이이 스레드에서 언급 함)를 피하는 데 도움이 될 수 있습니다.


답변

다음은 컴파일러가 모든 언어, 플랫폼, 컴파일러, 모든 문제와 같은 빠른 코드를 생성하는 데 도움이되는 코딩 방법입니다.

마십시오 하지 하는 힘있는 영리한 트릭을 사용하거나 심지어 가장 생각하는 (캐시 레지스터 포함) 메모리에 변수를 배치하는 컴파일러 바랍니다. 먼저 정확하고 유지 보수가 가능한 프로그램을 작성하십시오.

다음으로 코드를 프로파일 링하십시오.

그런 다음 컴파일러에게 메모리 사용 방법을 알려주는 효과를 조사하기 시작할 수 있습니다. 한 번에 하나씩 변경하고 그 영향을 측정합니다.

실망하고 작은 성능 향상을 위해 실제로 매우 열심히 노력해야 할 것으로 예상됩니다. Fortran 및 C와 같은 성숙한 언어를위한 최신 컴파일러는 매우 훌륭합니다. 코드에서 더 나은 성능을 얻기 위해 ‘트릭’에 대한 설명을 읽은 경우 컴파일러 작성자도 이에 대해 읽었으며 수행 할 가치가있는 경우이를 구현했을 수 있음을 명심하십시오. 그들은 아마도 당신이 처음 읽은 것을 썼을 것입니다.


답변

메모리를 순회하는 순서는 성능에 큰 영향을 미칠 수 있으며 컴파일러는 그것을 파악하고 수정하는 데 실제로 좋지 않습니다. 성능에 관심이 있다면 코드를 작성할 때 캐시 지역성 문제를 양심적으로 고려해야합니다. 예를 들어 C의 2 차원 배열은 행 주요 형식으로 할당됩니다. 열 주요 형식으로 배열을 순회하면 더 많은 캐시 미스가 발생하고 프로그램이 프로세서에 바인딩 된 것보다 더 많은 메모리에 바인딩되는 경향이 있습니다.

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

답변

일반 최적화

내가 가장 좋아하는 최적화 중 일부입니다. 나는 이것을 사용함으로써 실제로 실행 시간을 늘리고 프로그램 크기를 줄였습니다.

작은 함수를 inline또는 매크로 로 선언

함수 (또는 메서드)를 호출 할 때마다 변수를 스택에 푸시하는 것과 같은 오버 헤드가 발생합니다. 일부 함수는 반환시에도 오버 헤드가 발생할 수 있습니다. 비효율적 인 함수 또는 메서드는 결합 된 오버 헤드보다 콘텐츠에 더 적은 문을 포함합니다. #define매크로 든 inline함수 든 인라인하기에 좋은 후보입니다 . (예, inline제안 일 뿐이라는 것을 알고 있지만이 경우 컴파일러에 대한 알림 으로 간주합니다 .)

죽은 중복 코드 제거

코드가 사용되지 않거나 프로그램의 결과에 기여하지 않으면 제거하십시오.

알고리즘 설계 단순화

한 번은 계산중인 대수 방정식을 적어 프로그램에서 많은 어셈블리 코드와 실행 시간을 제거한 다음 대수식을 단순화했습니다. 단순화 된 대수식의 구현은 원래 함수보다 공간과 시간을 덜 차지했습니다.

루프 풀기

각 루프에는 증가 및 종료 검사의 오버 헤드가 있습니다. 성능 요인의 추정치를 얻으려면 오버 헤드의 명령어 수 (최소 3 : 증가, 확인, 루프 시작으로 이동)를 계산하고 루프 내의 명령문 수로 나눕니다. 숫자가 낮을수록 좋습니다.

편집 : 루프 풀기의 예를 제공 하십시오 .

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

펼친 후 :

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

이 이점에서 두 번째 이점이 있습니다. 프로세서가 명령 캐시를 다시로드하기 전에 더 많은 명령문이 실행됩니다.

32 개의 문에 대한 루프를 풀었을 때 놀라운 결과를 얻었습니다. 프로그램이 2GB 파일에서 체크섬을 계산해야했기 때문에 이것은 병목 현상 중 하나였습니다. 이 최적화는 블록 읽기와 결합되어 성능이 1 시간에서 5 분으로 향상되었습니다. 루프 언 롤링은 어셈블리 언어에서도 뛰어난 성능을 제공했으며 memcpy, 컴파일러의 memcpy. -TM

if진술의 축소

프로세서는 프로세서가 명령 대기열을 다시로드하도록하기 때문에 분기 또는 점프를 싫어합니다.

부울 연산 ( 편집 : 코드 조각에 코드 형식 적용, 예제 추가)

if문을 부울 할당으로 변환 합니다. 일부 프로세서는 분기없이 조건부로 명령을 실행할 수 있습니다.

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

단락논리 AND (오퍼레이터 &&)가이 경우, 테스트 실행 방지 status이다 false.

예:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

루프 외부 요인 변수 할당

루프 내에서 변수가 즉석에서 생성 된 경우 생성 / 할당을 루프 이전으로 이동합니다. 대부분의 경우 반복 할 때마다 변수를 할당 할 필요가 없습니다.

루프 외부에서 상수 표현식 인수

계산 또는 변수 값이 루프 인덱스에 의존하지 않는 경우 루프 외부 (앞)로 ​​이동하십시오.

블록의 I / O

큰 청크 (블록)로 데이터를 읽고 씁니다. 클수록 좋습니다. 예를 들어 한 에 한 옥텟을 읽는 것은 한 번의 읽기로 1024 옥텟을 읽는 것보다 덜 효율적입니다.
예:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

이 기술의 효율성은 시각적으로 입증 될 수 있습니다. 🙂

상수 데이터에 printf 패밀리 를 사용하지 마십시오.

블록 쓰기를 사용하여 상수 데이터를 출력 할 수 있습니다. 형식화 된 쓰기는 문자 형식화 또는 형식화 명령 처리를 위해 텍스트를 스캔하는 데 시간을 낭비합니다. 위의 코드 예제를 참조하십시오.

메모리로 포맷 한 다음 쓰기

char다중 sprintf을 사용 하여 배열로 포맷 한 다음 fwrite. 또한 데이터 레이아웃을 “상수 섹션”및 가변 섹션으로 나눌 수 있습니다. 메일 병합을 생각해보십시오 .

상수 텍스트 (문자열 리터럴)를 다음과 같이 선언합니다. static const

없이 변수를 선언하면 static일부 컴파일러는 스택에 공간을 할당하고 ROM에서 데이터를 복사 할 수 있습니다. 이것은 두 가지 불필요한 작업입니다. static접두사 를 사용하여 해결할 수 있습니다 .

마지막으로 컴파일러와 같은 코드는

때로는 컴파일러가 하나의 복잡한 버전보다 여러 개의 작은 문을 더 잘 최적화 할 수 있습니다. 또한 컴파일러가 최적화하는 데 도움이되는 코드를 작성하는 것도 도움이됩니다. 컴파일러가 특수 블록 전송 명령어를 사용하도록하려면 특수 명령어를 사용해야하는 것처럼 보이는 코드를 작성합니다.


답변

최적화 프로그램은 실제로 프로그램의 성능을 제어하지 않습니다. 적절한 알고리즘 및 구조와 프로필, 프로필, 프로필을 사용합니다.

즉, 다른 파일에있는 한 파일의 작은 함수에 대해 내부 루프를 수행하면 안됩니다. 이렇게하면 인라인되지 않습니다.

가능하면 변수의 주소를 사용하지 마십시오. 포인터를 요청하는 것은 변수가 메모리에 유지되어야하기 때문에 “무료”가 아닙니다. 포인터를 피하면 배열조차도 레지스터에 보관할 수 있습니다. 이것은 벡터화에 필수적입니다.

다음 요점으로 연결 되는 ^ # $ @ 설명서를 읽으십시오 ! GCC는 __restrict__여기 __attribute__( __aligned__ )저기 뿌리면 일반 C 코드를 벡터화 할 수 있습니다 . 옵티 마이저에서 매우 특정한 것을 원한다면 구체적이어야 할 수도 있습니다.


답변

대부분의 최신 프로세서에서 가장 큰 병목 현상은 메모리입니다.

Aliasing : Load-Hit-Store는 타이트한 루프에서 치명적일 수 있습니다. 한 메모리 위치를 읽고 다른 메모리 위치에 쓰고 있고 이들이 분리되어 있음을 알고 있다면 함수 매개 변수에 별칭 키워드를 신중하게 배치하면 컴파일러가 더 빠른 코드를 생성하는 데 실제로 도움이 될 수 있습니다. 그러나 메모리 영역이 겹치고 ‘별칭’을 사용한 경우 정의되지 않은 동작에 대한 좋은 디버깅 세션이 있습니다!

Cache-miss : 대부분 알고리즘이기 때문에 컴파일러를 어떻게 도울 수 있는지 확실하지 않지만 메모리를 프리 페치하는 내장 기능이 있습니다.

또한 부동 소수점 값을 int로 변환하거나 그 반대로 너무 많이 변환하지 마십시오. 다른 레지스터를 사용하고 한 유형에서 다른 유형으로 변환하는 것은 실제 변환 명령어를 호출하고 값을 메모리에 쓰고 적절한 레지스터 세트에서 다시 읽는 것을 의미합니다. .


답변

사람들이 작성하는 대부분의 코드는 I / O 바운드 (지난 30 년 동안 돈을 위해 작성한 모든 코드가 너무 바운드되었다고 생각합니다)이므로 대부분의 사람들을위한 옵티마이 저의 활동은 학문적 일 것입니다.

그러나 코드를 최적화하려면 컴파일러에게 최적화하도록 지시해야한다는 점을 사람들에게 상기시키고 싶습니다. 많은 사람들 (잊었을 때 저를 포함하여)이 여기에 옵티 마이저를 활성화하지 않으면 의미가없는 C ++ 벤치 마크를 게시합니다.