Programing

컴파일러 / 최적화 프로그램이 더 빠른 프로그램을 만들 수있게하는 코딩 방법

lottogame 2020. 7. 19. 09:47
반응형

컴파일러 / 최적화 프로그램이 더 빠른 프로그램을 만들 수있게하는 코딩 방법


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

시간이 지남에 따라 컴파일러는 발전했습니다. 흐름 분석을 통해 레지스터에서 보유 할 값보다 더 나은 결정을 내릴 수 있다는 점에서 매우 현명 해졌습니다. 레지스터 키워드가 중요하지 않게되었습니다.

별명 문제 로 인해 일부 유형의 조작에서 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

큰 덩어리 (블록)로 데이터를 읽고 씁니다. 클수록 좋습니다. 예를 들어, 한 번에 하나의 octect읽는 것이 한 번의 읽기로 1024 개의 octet를 읽는 것보다 덜 효율적입니다.
예:

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 패밀리사용하지 마십시오

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

메모리로 포맷 한 후 쓰기

charmultiple sprintf을 사용 하여 배열로 포맷 한 다음을 사용하십시오 fwrite. 또한 데이터 레이아웃을 "일정한 섹션"과 가변 섹션으로 나눌 수 있습니다. 편지 병합 생각하십시오 .

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

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

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

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


옵티마이 저는 실제로 프로그램 성능을 제어하지 않습니다. 적절한 알고리즘 및 구조와 프로파일, 프로파일, 프로파일을 사용하십시오.

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

가능하면 변수의 주소를 사용하지 마십시오. 변수를 메모리에 보관해야하므로 포인터를 요구하는 것은 "무료"가 아닙니다. 포인터를 피하면 배열을 레지스터에 유지할 수 있습니다. 이는 벡터화에 필수적입니다.

다음 포인트로 이어지는 ^ # $ @ 매뉴얼을 읽어보십시오 ! GCC는 __restrict__여기 __attribute__( __aligned__ )저기에 뿌리면 일반 C 코드를 벡터화 할 수 있습니다 . 옵티 마이저에서 매우 구체적인 것을 원하면 구체적이어야 할 수도 있습니다.


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

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

캐시 미스 : 컴파일러가 주로 알고리즘이기 때문에 컴파일러를 도울 수있는 방법을 확실하지 않지만 메모리를 미리 가져 오는 본질적인 요소가 있습니다.

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


사람들이 작성하는 대부분의 코드는 I / O 바인딩이 될 것입니다 (지난 30 년 동안 돈을 위해 작성한 모든 코드가 너무 구속력이 있다고 생각합니다). 따라서 대부분의 사람들을위한 옵티마이 저의 활동은 학업이 될 것입니다.

그러나 사람들에게 코드를 최적화하려면 컴파일러에게 최적화하도록 지시해야한다고 생각합니다. 최적화 장치를 사용하지 않으면 의미가없는 C ++ 벤치 마크를 게시하는 많은 사람들이 여기에 있습니다.


코드에서 가능한 한 const 정확성을 사용하십시오. 컴파일러가 훨씬 더 최적화 할 수 있습니다.

이 문서에는 다른 최적화 팁이 많이 있습니다. CPP 최적화 (약간 오래된 문서)

하이라이트:

  • 생성자 초기화 목록 사용
  • 접두사 연산자 사용
  • 명시 적 생성자를 사용
  • 인라인 함수
  • 임시 객체를 피하십시오
  • 가상 기능의 비용을 인식
  • 참조 매개 변수를 통해 객체를 반환
  • 수업 당 배정
  • 컨테이너 컨테이너 할당자를 고려하십시오
  • '빈 멤버'최적화
  • 기타

정적 단일 할당을 사용하여 가능한 많이 프로그래밍하십시오. SSA는 대부분의 함수형 프로그래밍 언어에서 사용하는 것과 정확히 동일하며, 대부분의 컴파일러는 작업하기가 쉽기 때문에 최적화를 위해 코드를 변환합니다. 컴파일러가 혼란 스러울 수있는 곳을 수행하면 조명이 켜집니다. 또한 최악의 레지스터 할당자를 제외한 모든 레지스터가 최상의 레지스터 할당 자만큼 훌륭하게 작동하며, 변수가 할당 된 위치가 하나뿐이므로 변수의 위치를 ​​궁금해 할 필요가 없기 때문에 더 쉽게 디버깅 할 수 있습니다.
전역 변수를 피하십시오.

참조 또는 포인터로 데이터를 작업 할 때는 해당 변수를 로컬 변수로 가져 와서 작업을 한 다음 다시 복사하십시오. (당신이하지 않는 좋은 이유가 없다면)

수학 또는 논리 연산을 수행 할 때 대부분의 프로세서가 제공하는 0에 대한 거의 무료 비교를 사용하십시오. 거의 항상 == 0 및 <0에 대한 플래그를 얻습니다.이 플래그에서 3 가지 조건을 쉽게 얻을 수 있습니다.

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

다른 상수를 테스트하는 것보다 거의 항상 저렴합니다.

또 다른 요령은 뺄셈을 사용하여 범위 테스트에서 한 비교를 제거하는 것입니다.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

이렇게하면 부울 식에서 단락을 일으키는 언어의 점프를 피할 수 있으며 컴파일러가 첫 번째 비교 결과를 처리 한 다음이를 조합하여 처리하는 방법을 알아 내지 않아도됩니다. 이것은 추가 레지스터를 사용할 가능성이있는 것처럼 보이지만 거의 수행하지는 않습니다. 어쨌든 더 이상 foo가 필요하지 않으며 rc를 사용하지 않으면 아직 갈 수 없습니다.

c (strcpy, memcpy, ...)에서 문자열 함수를 사용할 때 반환되는 것을 기억하십시오-목적지! 대상에 대한 포인터 사본을 '잊어 버림'하여 더 나은 코드를 얻을 수 있으며 이러한 함수의 반환에서 다시 가져옵니다.

마지막으로 호출 한 함수가 반환 한 것과 똑같은 결과를 반환 할 가능성을 간과하지 마십시오. 컴파일러는 다음을 수행하는 데별로 좋지 않습니다.

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

물론 하나의 리턴 포인트 만있는 경우 해당 로직을 반대로 바꿀 수 있습니다.

(나중에 기억했던 트릭)

가능하면 함수를 정적으로 선언하는 것이 좋습니다. 컴파일러가 특정 함수의 모든 호출자를 설명했음을 입증 할 수 있으면 최적화 이름으로 해당 함수의 호출 규칙을 위반할 수 있습니다. 컴파일러는 종종 호출 된 함수가 매개 변수가있는 것으로 예상되는 레지스터 또는 스택 위치로 매개 변수를 이동하지 않도록 할 수 있습니다 (호출 된 함수와이를 수행하기 위해 모든 호출자의 위치에서 벗어나야 함). 컴파일러는 종종 호출 된 함수가 필요로하는 메모리와 레지스터를 아는 이점을 이용하고 호출 된 함수가 방해하지 않는 레지스터 또는 메모리 위치에있는 변수 값을 보존하기위한 코드 생성을 피할 수 있습니다. 함수에 대한 호출이 적을 때 특히 효과적입니다. 이것은 코드 인라이닝의 이점을 많이 얻습니다.


나는 최적화 C 컴파일러를 작성했으며 여기에 고려해야 할 매우 유용한 것들이 있습니다.

  1. 대부분의 함수를 정적으로 만듭니다. 이를 통해 프로 시저 간 상수 전파 및 별명 분석이 해당 작업을 수행 할 수 있습니다. 그렇지 않으면 컴파일러는 매개 변수에 대한 값을 완전히 알 수없는 변환 단위 외부에서 함수를 호출 할 수 있다고 가정해야합니다. 잘 알려진 오픈 소스 라이브러리를 살펴보면 실제로 extern 해야하는 것을 제외하고는 모두 정적 함수로 표시됩니다.

  2. 글로벌 변수가 사용되는 경우 가능하면 정적 및 상수로 표시하십시오. 그것들이 한 번 초기화되면 (읽기 전용) static const int VAL [] = {1,2,3,4}와 같은 초기화 목록을 사용하는 것이 좋습니다. 그렇지 않으면 컴파일러가 변수가 실제로 초기화 된 상수임을 발견하지 못할 수 있습니다. 변수의 하중을 상수로 대체하지 못합니다.

  3. 루프 내부로 이동을 사용하지 마십시오. 대부분의 컴파일러는 루프를 더 이상 인식하지 못하며 가장 중요한 최적화는 적용되지 않습니다.

  4. 필요한 경우에만 포인터 매개 변수를 사용하고 가능하면 제한으로 표시하십시오. 이것은 프로그래머가 별칭이 없음을 보장하기 때문에 별칭 분석에 많은 도움이됩니다 (절차 적 절차는 일반적으로 매우 원시적입니다). 매우 작은 구조체 객체는 참조가 아닌 값으로 전달해야합니다.

  5. 가능하면 포인터, 특히 루프 내부 (a [i]) 대신 배열을 사용하십시오. 배열은 일반적으로 별칭 분석에 대한 자세한 정보를 제공하며 일부 최적화 후에도 동일한 코드가 생성됩니다 (호기심이 많으면 루프 강도 감소 검색). 또한 루프 불변 코드 모션이 적용될 가능성이 높아집니다.

  6. 큰 함수 나 부작용이없는 외부 함수에 대한 루프 호출을 외부로 들어 올리십시오 (현재 루프 반복에 의존하지 않음). 작은 함수는 대부분의 경우 인라인으로 변환되거나 호이스트하기 쉬운 내장 함수로 변환되지만, 큰 함수는 컴파일러가 실제로 그렇지 않은 경우 부작용을 갖는 것처럼 보일 수 있습니다. 표준 라이브러리의 일부 함수를 제외하고 외부 컴파일러에 대한 부작용은 완전히 알려지지 않았으며, 일부 컴파일러에 의해 때때로 모델링되어 루프 불변 코드 동작을 가능하게합니다.

  7. 여러 조건으로 테스트를 작성할 때 가장 가능성이 높은 것을 먼저 배치하십시오. 만약 b 가 다른 것보다 사실 일 가능성이 높으면 if (a || b || c)는 if (b || a || c) 이어야합니다. 컴파일러는 일반적으로 가능한 조건 값과 더 많은 분기를 알지 못합니다 (프로파일 정보를 사용하여 알 수는 있지만 프로그래머는 거의 사용하지 않습니다).

  8. if (a || b || ... || z)와 같은 테스트를 수행하는 것보다 스위치를 사용하는 것이 더 빠릅니다. 컴파일러가 자동 으로이 작업을 수행하는지 먼저 확인하고 일부는 if 를 사용하는 것이 더 읽기 쉽습니다.


임베디드 시스템과 C / C ++로 작성된 코드의 경우 가능한 동적 메모리 할당피하려고 노력 합니다. 내가 이것을하는 주된 이유는 반드시 성능 일 필요는 없지만이 경험 규칙은 성능에 영향을 미칩니다.

힙을 관리하는 데 사용되는 알고리즘은 일부 플랫폼 (예 : vxworks)에서 매우 느립니다. 더 나쁜 것은, malloc 호출에서 되돌아 오는 데 걸리는 시간은 힙의 현재 상태에 따라 크게 달라집니다. 따라서 malloc을 호출하는 모든 함수는 쉽게 설명 할 수없는 성능 저하를 가져옵니다. 힙이 여전히 깨끗하지만 해당 장치가 잠시 동안 실행 된 후 힙이 조각화 될 수있는 경우 성능 저하가 최소화 될 수 있습니다. 통화 시간이 오래 걸리며 시간이 지남에 따라 성능이 저하되는 방식을 쉽게 계산할 수 없습니다. 실제로 더 나쁜 사례 추정을 생성 할 수는 없습니다. 이 경우에도 옵티마이 저가 도움을 제공 할 수 없습니다. 설상가상으로 힙이 너무 세분화되면 호출이 모두 실패하기 시작합니다. 해결책은 메모리 풀을 사용하는 것입니다 (예 :힙 대신 glib slices ). 할당 호출은 올바르게 수행하면 훨씬 빠르고 결정적입니다.


멍청한 작은 팁이지만 미세한 속도와 코드를 절약 할 수있는 팁입니다.

항상 같은 순서로 함수 인수를 전달하십시오.

f_2를 호출하는 f_1 (x, y, z)가 있으면 f_2를 f_2 (x, y, z)로 선언하십시오. f_2 (x, z, y)로 선언하지 마십시오.

그 이유는 C / C ++ 플랫폼 ABI (AKA 호출 규칙)가 특정 레지스터 및 스택 위치에서 인수를 전달할 것이기 때문입니다. 인수가 이미 올바른 레지스터에 있으면 이동할 필요가 없습니다.

분해 된 코드를 읽는 동안 사람들 이이 규칙을 따르지 않았기 때문에 어리석은 레지스터 셔플 링을 보았습니다.


위 목록에서 보지 못한 두 가지 코딩 기술 :

고유 한 소스로 코드를 작성하여 링커 우회

별도의 컴파일은 컴파일 시간에 정말 좋지만 최적화에 관해서는 매우 나쁩니다. 기본적으로 컴파일러는 컴파일 단위 (링커 예약 도메인) 이상으로 최적화 할 수 없습니다.

그러나 프로그램을 잘 디자인하면 고유 한 공통 소스를 통해 컴파일 할 수도 있습니다. 즉, unit1.c와 unit2.c를 컴파일하는 대신 두 객체를 연결하고 #include unit1.c와 unit2.c 인 all.c를 컴파일하십시오. 따라서 모든 컴파일러 최적화의 이점이 있습니다.

C ++로 헤더 전용 프로그램을 작성하는 것과 매우 비슷합니다 (C에서는 훨씬 더 쉽습니다).

이 기술은 처음부터 활성화 할 수 있도록 프로그램을 작성하면 충분하지만 C 시맨틱의 일부를 변경하고 정적 변수 또는 매크로 충돌과 같은 일부 문제를 해결할 수 있다는 점도 알고 있어야합니다. 대부분의 프로그램에서 발생하는 작은 문제를 쉽게 극복 할 수 있습니다. 또한 고유 한 소스로 컴파일하는 것은 속도가 느리고 많은 양의 메모리가 필요할 수 있습니다 (일반적으로 최신 시스템에서는 문제가되지 않음).

이 간단한 기술을 사용하여 10 배나 빠른 프로그램을 만들었습니다!

register 키워드와 마찬가지로이 트릭도 곧 폐기 될 수 있습니다. 링커를 통한 최적화는 컴파일러 gcc : 링크 시간 최적화에 의해 지원되기 시작합니다 .

루프에서 개별 원자 작업

이것은 더 까다 롭습니다. 알고리즘 설계와 옵티마이 저가 캐시 및 레지스터 할당을 관리하는 방식 간의 상호 작용에 관한 것입니다. 종종 프로그램은 일부 데이터 구조를 반복해야하며 각 항목마다 일부 작업을 수행해야합니다. 수행되는 작업은 논리적으로 독립적 인 두 작업으로 나눌 수 있습니다. 이 경우 정확히 하나의 작업을 수행하는 동일한 경계에 두 개의 루프가있는 동일한 프로그램을 정확하게 작성할 수 있습니다. 어떤 경우에는이 방법을 고유 루프보다 빠를 수 있습니다 (세부 사항은 더 복잡하지만 간단한 작업의 경우 모든 변수를 프로세서 레지스터에 유지할 수 있으며 더 복잡한 변수는 불가능하며 일부는 설명 할 수 있습니다 레지스터는 메모리에 쓰고 나중에 다시 읽어야하며 비용은 추가 흐름 제어보다 높습니다.

레지스터를 사용하는 것처럼이 기능 (이 트릭을 사용하는 프로파일 성능 또는 사용 안함)에주의하십시오. 개선 된 것보다 성능이 떨어질 수 있습니다.


실제로 SQLite 에서이 작업을 수행했으며 성능이 ~ 5 % 향상된다고 주장합니다. 모든 코드를 하나의 파일에 넣거나 전처리기를 사용하여 이와 동등한 작업을 수행하십시오. 이런 식으로 옵티마이 저는 전체 프로그램에 액세스 할 수 있으며보다 많은 절차 적 최적화를 수행 할 수 있습니다.


대부분의 최신 컴파일러는 함수 호출을 최적화 할 수 있기 때문에 테일 재귀 속도를 높이는 훌륭한 작업을 수행해야 합니다.

예:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

물론이 예제에는 경계 검사가 없습니다.

늦은 편집

코드에 대한 직접적인 지식은 없지만; SQL Server에서 CTE를 사용하기위한 요구 사항은 테일 엔드 재귀를 통해 최적화 할 수 있도록 특별히 설계된 것 같습니다.


같은 작업을 반복해서하지 마십시오!

내가 볼 수있는 일반적인 반 패턴은 다음과 같습니다.

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

컴파일러는 실제로 모든 함수를 항상 호출해야합니다. 프로그래머가 당신을 가정 할 때, 거룩한 모든 것을 사랑하기 위해 집계 된 객체가 이러한 호출 과정에서 바뀌지 않는다는 것을 알고 있습니다 ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

싱글 톤 게터의 경우 호출 비용이 너무 많이 들지 않지만 확실히 비용이 듭니다 (일반적으로 "객체가 생성되었는지 확인한 후 생성하지 않은 경우 생성 한 다음 반환)." 이 게터 체인이 복잡해지면 더 많은 시간을 낭비하게됩니다.


  1. 모든 변수 선언에 가능한 가장 로컬 범위를 사용하십시오.

  2. const가능할 때마다 사용

  3. 청춘의 사용 레지스터 당신과 함께하고 그것없이 모두 프로파일 계획이 아니라면

The first 2 of these, especially #1 one help the optimizer analyze the code. It will especially help it to make good choices about what variables to keep in registers.

Blindly using the register keyword is as likely to help as hurt your optimization, It's just too hard to know what will matter until you look at the assembly output or profile.

There are other things that matter to getting good performance out of code; designing your data structures to maximize cache coherency for instance. But the question was about the optimizer.


Align your data to native/natural boundaries.


I was reminded of something that I encountered once, where the symptom was simply that we were running out of memory, but the result was substantially increased performance (as well as huge reductions in memory footprint).

The problem in this case was that the software we were using made tons of little allocations. Like, allocating four bytes here, six bytes there, etc. A lot of little objects, too, running in the 8-12 byte range. The problem wasn't so much that the program needed lots of little things, it's that it allocated lots of little things individually, which bloated each allocation out to (on this particular platform) 32 bytes.

Part of the solution was to put together an Alexandrescu-style small object pool, but extend it so I could allocate arrays of small objects as well as individual items. This helped immensely in performance as well since more items fit in the cache at any one time.

The other part of the solution was to replace the rampant use of manually-managed char* members with an SSO (small-string optimization) string. The minimum allocation being 32 bytes, I built a string class that had an embedded 28-character buffer behind a char*, so 95% of our strings didn't need to do an additional allocation (and then I manually replaced almost every appearance of char* in this library with this new class, that was fun or not). This helped a ton with memory fragmentation as well, which then increased the locality of reference for other pointed-to objects, and similarly there were performance gains.


A neat technique I learned from @MSalters comment on this answer allows compilers to do copy elision even when returning different objects according to some condition:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

If you've got small functions you call repeatedly, i have in the past got large gains by putting them in headers as "static inline". Function calls on the ix86 are surprisingly expensive.

Reimplementing recursive functions in a non-recursive way using an explicit stack can also gain a lot, but then you really are in the realm of development time vs gain.


Here's my second piece of optimisation advice. As with my first piece of advice this is general purpose, not language or processor specific.

Read the compiler manual thoroughly and understand what it is telling you. Use the compiler to its utmost.

I agree with one or two of the other respondents who have identified selecting the right algorithm as critical to squeezing performance out of a program. Beyond that the rate of return (measured in code execution improvement) on the time you invest in using the compiler is far higher than the rate of return in tweaking the code.

Yes, compiler writers are not from a race of coding giants and compilers contain mistakes and what should, according to the manual and according to compiler theory, make things faster sometimes makes things slower. That's why you have to take one step at a time and measure before- and after-tweak performance.

And yes, ultimately, you might be faced with a combinatorial explosion of compiler flags so you need to have a script or two to run make with various compiler flags, queue the jobs on the large cluster and gather the run time statistics. If it's just you and Visual Studio on a PC you will run out of interest long before you have tried enough combinations of enough compiler flags.

Regards

Mark

When I first pick up a piece of code I can usually get a factor of 1.4 -- 2.0 times more performance (ie the new version of the code runs in 1/1.4 or 1/2 of the time of the old version) within a day or two by fiddling with compiler flags. Granted, that may be a comment on the lack of compiler savvy among the scientists who originate much of the code I work on, rather than a symptom of my excellence. Having set the compiler flags to max (and it's rarely just -O3) it can take months of hard work to get another factor of 1.05 or 1.1


When DEC came out with its alpha processors, there was a recommendation to keep the number of arguments to a function under 7, as the compiler would always try to put up to 6 arguments in registers automatically.


For performance, focus first on writing maintenable code - componentized, loosely coupled, etc, so when you have to isolate a part either to rewrite, optimize or simply profile, you can do it without much effort.

Optimizer will help your program's performance marginally.


You're getting good answers here, but they assume your program is pretty close to optimal to begin with, and you say

Assume that the program has been written correctly, compiled with full optimization, tested and put into production.

In my experience, a program may be written correctly, but that does not mean it is near optimal. It takes extra work to get to that point.

If I can give an example, this answer shows how a perfectly reasonable-looking program was made over 40 times faster by macro-optimization. Big speedups can't be done in every program as first written, but in many (except for very small programs), it can, in my experience.

After that is done, micro-optimization (of the hot-spots) can give you a good payoff.


i use intel compiler. on both Windows and Linux.

when more or less done i profile the code. then hang on the hotspots and trying to change the code to allow compiler make a better job.

if a code is a computational one and contain a lot of loops - vectorization report in intel compiler is very helpful - look for 'vec-report' in help.

so the main idea - polish the performance critical code. as for the rest - priority to be correct and maintainable - short functions, clear code that could be understood 1 year later.


One optimization i have used in C++ is creating a constructor that does nothing. One must manually call an init() in order to put the object into a working state.

This has benefit in the case where I need a large vector of these classes.

I call reserve() to allocate the space for the vector, but the constructor does not actually touch the page of memory the object is on. So I have spent some address space, but not actually consumed a lot of physical memory. I avoid the page faults associated the associated construction costs.

As i generate objects to fill the vector, I set them using init(). This limits my total page faults, and avoids the need to resize() the vector while filling it.


One thing I've done is try to keep expensive actions to places where the user might expect the program to delay a bit. Overall performance is related to responsiveness, but isn't quite the same, and for many things responsiveness is the more important part of performance.

The last time I really had to do improvements in overall performance, I kept an eye out for suboptimal algorithms, and looked for places that were likely to have cache problems. I profiled and measured performance first, and again after each change. Then the company collapsed, but it was interesting and instructive work anyway.


I have long suspected, but never proved that declaring arrays so that they hold a power of 2, as the number of elements, enables the optimizer to do a strength reduction by replacing a multiply by a shift by a number of bits, when looking up individual elements.


Put small and/or frequently called functions at the top of the source file. That makes it easier for the compiler to find opportunities for inlining.

참고URL : https://stackoverflow.com/questions/2074099/coding-practices-which-enable-the-compiler-optimizer-to-make-a-faster-program

반응형