IT이야기

컴파일러/옵티마이저를 통한 프로그램 고속화를 가능하게 하는 코딩 프랙티스

cyworld 2022. 6. 8. 23:42
반응형

컴파일러/옵티마이저를 통한 프로그램 고속화를 가능하게 하는 코딩 프랙티스

몇 년 전만 해도 C 컴파일러는 그다지 똑똑하지 않았다.회피책으로서 K&R은 컴파일러에게 이 변수를 내부 레지스터에 보관하는 것이 좋을 것이라는 힌트를 주기 위해 register 키워드를 발명했습니다.그들은 또한 더 나은 코드를 생성하는 것을 돕기 위해 3차 연산자를 만들었다.

시간이 지남에 따라 컴파일러들은 성숙해졌다.플로우 분석을 통해 레지스터에 어떤 값을 보유할지 사용자가 할 수 있는 것보다 더 나은 결정을 내릴 수 있다는 점에서 매우 현명해졌습니다.register 키워드는 중요하지 않게 되었습니다.

FORTRAN은 에일리어스 문제로 인해 어떤 종류의 조작에서는 C보다 빠를 수 있습니다.이론적으로는 신중하게 코딩하면 이 제한을 회피하여 최적기가 더 빠른 코드를 생성할 수 있습니다.

컴파일러/옵티마이저가 더 빠른 코드를 생성할 수 있도록 하기 위해 사용할 수 있는 코딩 방법은 무엇입니까?

  • 사용하고 있는 플랫폼과 컴파일러를 특정해 주시면 감사하겠습니다.
  • 이 기술은 왜 효과가 있는 것 같습니까?
  • 샘플 코드를 권장합니다.

여기 관련된 질문이 있습니다.

[편집] 이 질문은 프로파일링과 최적화에 관한 전반적인 프로세스가 아닙니다.프로그램이 올바르게 작성되고 완전히 최적화되어 컴파일되고 테스트되어 실제 가동에 투입되었다고 가정합니다.코드에는 최적기가 최고의 작업을 수행할 수 없도록 하는 구성 요소가 있을 수 있습니다.이러한 금지사항을 제거하고 옵티마이저가 더 빠른 코드를 생성할 수 있도록 하는 리팩터에는 무엇을 할 수 있습니까?

[편집] 오프셋 관련 링크

범용 최적화

여기 제가 가장 좋아하는 최적화가 있습니다.실제로 이를 사용하여 실행 시간을 늘리고 프로그램 크기를 줄였습니다.

를 「」라고 합니다.inline 매크로(「」)

함수(또는 메서드)에 대한 각 호출은 변수를 스택에 푸시하는 등의 오버헤드를 발생시킵니다.일부 기능은 반환 시 오버헤드가 발생할 수도 있습니다.비효율적인 함수 또는 메서드는 결합된 오버헤드보다 내용에 대한 문장이 적습니다.할 수 있는 입니다.#define 또는 " " " 입니다.inline, 알아요.inline제안일 뿐이지만, 이 경우는 컴파일러에 대한 주의사항으로 간주합니다.)

데드 코드 및 용장 코드를 삭제합니다.

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

알고리즘 설계 단순화

한때는 프로그램이 계산하고 있는 대수 방정식을 적어 프로그램에서 많은 어셈블리 코드와 실행 시간을 제거한 후 대수식을 단순화했습니다.단순화된 대수식의 구현은 원래 함수보다 더 적은 공간과 시간을 차지했습니다.

루프 언롤링

각 루프에는 증분 및 종단 체크의 오버헤드가 있습니다.퍼포먼스 팩터의 견적을 얻으려면 오버헤드 내의 명령 수(최소 3: increment, check, goto start of loop)를 카운트하고 루프 내의 스테이트먼트 수로 나눕니다.숫자가 작을수록 좋다.

편집: 루프 롤링의 예를 다음에 제시합니다.

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파 the the보다 memcpy. - T.M.

if(START)

프로세서는 명령 큐를 강제로 새로고침하기 때문에 브랜치를 싫어하거나 점프합니다.

Boolean 산술(편집: 코드 프래그먼트에 적용된 코드 형식, 추가 예)

★★if스테이트먼트를 부울 할당으로 변환합니다.일부 프로세서는 분기하지 않고 조건부로 명령을 실행할 수 있습니다.

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

논리적 AND 연산자의 단락(&&)는 을(를) 사용했을 경우, 의 실행을 합니다.statusfalse.

예:

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

데이터를 큰 청크(블록)로 읽고 씁니다.크면 클수록 좋다.예를 들어, 한 번에 하나의 옥텟을 읽는 것은 1개의 판독으로 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에서 데이터를 복사할 수 있습니다.2번는 '하다'를 사용하여 할 수 .static프레픽스

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

컴파일러는 복잡한 버전보다 여러 개의 작은 문장을 최적화할 수 있습니다.또, 컴파일러의 최적화에 도움이 되는 코드를 쓰는 것도 도움이 됩니다.컴파일러가 특별한 블록 전송 명령을 사용하는 경우 특별한 명령을 사용하는 것처럼 보이는 코드를 작성합니다.

로컬 변수에 쓰고 인수는 출력하지 마십시오!이것은 에일리어스 슬로우 회피에 큰 도움이 됩니다.예를 들어, 코드가 다음과 같은 경우

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;
}

우습게 들리지만, 컴파일러는 로컬 변수를 처리하는 것이 훨씬 더 현명할 수 있습니다. 왜냐하면 어떤 인수와도 메모리에 중복될 수 없기 때문입니다.이렇게 하면 (프란시스 보이빈이 이 스레드에서 언급한) 무서운 로드 히트 스토어를 피할 수 있습니다.

다음은 컴파일러가 빠른 코드(언어, 플랫폼, 컴파일러, 문제 등)를 작성할 수 있도록 지원하는 코딩 프랙티스입니다.

컴파일러가 최적의 메모리(캐시 및 레지스터 포함)에 변수를 배치하도록 강제하거나 권장하는 교묘한 속임수를 사용하지 마십시오.우선 정확하고 유지보수가 가능한 프로그램을 작성하세요.

다음으로 코드를 프로파일합니다.

그런 다음 컴파일러에 메모리 사용법을 지시했을 때의 영향에 대해 조사를 시작하는 것이 좋습니다.한 번에 하나씩 변화를 주고 그 영향을 측정하세요.

실망하고 작은 성능 향상을 위해 정말 열심히 일해야 합니다.Fortran이나 C와 같은 성숙한 언어용 최신 컴파일러는 매우 좋습니다.코드로부터 퍼포먼스를 향상시키기 위한 「꼼수」의 어카운트를 읽는 경우는, 컴파일러의 라이터도 읽어, 실행할 가치가 있는 경우는, 아마 실장하고 있는 것에 주의해 주세요.애초에 읽었던 걸 썼을 거예요.

가능한 한 정적 단일 할당을 사용하여 프로그래밍을 시도합니다.SSA는 대부분의 기능적인 프로그래밍 언어에서 사용되는 것과 완전히 동일합니다.또한 대부분의 컴파일러가 코드를 변환하여 최적화를 수행합니다.이것은 사용하기 쉽기 때문입니다.이렇게 하면 컴파일러가 혼란스러워질 수 있습니다.또한 최악의 레지스터 할당자를 제외한 모든 레지스터 할당자가 최고의 레지스터 할당자만큼 잘 작동하며 변수가 할당된 위치가 하나뿐이기 때문에 변수의 값을 어디서 얻었는지 알 필요가 거의 없기 때문에 더 쉽게 디버깅할 수 있습니다.
전역 변수를 피합니다.

참조 또는 포인터로 데이터를 작업하는 경우 로컬 변수에 해당 데이터를 끌어다 놓고 작업을 수행한 다음 다시 복사합니다.(당신이 하지 않을 정당한 이유가 없는 한)

대부분의 프로세서가 연산 또는 논리 연산을 수행할 때 제공하는 0과 거의 자유로운 비교를 사용합니다.거의 항상 ==0 및 <0에 대한 플래그를 얻을 수 있으며, 이 플래그는 다음과 같은 세 가지 조건을 쉽게 얻을 수 있습니다.

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;
}

물론 리턴 포인트가 1개뿐이라면 논리를 뒤집을 수 있습니다.

(나중에 생각난 트릭)

가능한 경우 기능을 스태틱으로 선언하는 것이 좋습니다.컴파일러가 특정 함수의 모든 호출자를 처리했음을 스스로 증명할 수 있다면 최적화라는 이름으로 해당 함수에 대한 호출 규칙을 깰 수 있습니다.컴파일러는 보통 호출된 함수가 자신의 파라미터가 있을 것으로 예상되는 레지스터 또는 스택 위치로 파라미터를 이동하는 것을 피할 수 있습니다(이 처리를 위해서는 호출된 함수와 모든 발신자의 위치에서 모두 벗어날 필요가 있습니다).컴파일러는 또한 호출된 함수가 필요로 하는 메모리와 레지스터를 알고 있으며 호출된 함수가 방해하지 않는 레지스터나 메모리 위치에 있는 변수 값을 보존하기 위한 코드 생성을 피할 수 있습니다.이것은, 함수에의 콜이 적은 경우에 특히 유효하게 동작합니다.이것은 인라이닝 코드의 메리트를 많이 얻을 수 있지만, 실제로는 인라이닝 하지 않습니다.

메모리를 통과하는 순서는 퍼포먼스에 큰 영향을 미칠 수 있습니다.컴파일러는 이 문제를 파악하고 수정하는 데 그다지 능숙하지 않습니다.성능에 관심이 있는 경우 코드를 작성할 때 캐시 인접성에 대한 우려를 의식해야 합니다.예를 들어 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];
  }
}

C/C++로 작성된 임베디드 시스템이나 코드는 가능한 한 동적 메모리 할당을 피하려고 합니다.제가 이렇게 하는 주된 이유는 반드시 퍼포먼스 때문은 아니지만 이 경험의 법칙은 퍼포먼스에 영향을 미칩니다.

힙 관리에 사용되는 알고리즘은 일부 플랫폼(예: vxworks)에서 느리기로 악명 높습니다.게다가 콜로부터 malloc 로의 복귀에 걸리는 시간은, 히프의 현재 상태에 의해서 크게 좌우됩니다.따라서 malloc을 호출하는 함수는 쉽게 설명할 수 없는 퍼포먼스에 타격을 입게 됩니다.히프가 아직 깨끗한 경우 퍼포먼스에 미치는 영향은 미미할 수 있습니다.단, 디바이스 실행 후 잠시 동안 히프가 fragment화될 수 있습니다.콜은 시간이 오래 걸리기 때문에 시간이 지남에 따라 퍼포먼스가 저하되는 것을 쉽게 계산할 수 없습니다.이보다 더 나쁜 경우의 추정치는 나올 수 없습니다.이 경우에도 옵티마이저는 어떤 도움말도 제공할 수 없습니다.설상가상으로 힙이 너무 fragment화되면 콜이 완전히 실패하기 시작합니다.해결책은 힙 대신 메모리 풀(glib 슬라이스 등)을 사용하는 것입니다.올바르게 처리하면 할당 호출이 훨씬 빠르고 확정적입니다.

위의 리스트에서 볼 수 없었던2가지 코딩 기술:

코드를 고유한 소스로 작성하여 링커를 바이패스합니다.

개별 컴파일은 컴파일 시간에는 매우 좋지만 최적화에 대해서는 매우 좋지 않습니다.기본적으로 컴파일러는 컴파일 유닛(링커 예약 도메인) 이상을 최적화할 수 없습니다.

그러나 프로그램을 잘 설계하면 고유한 공통 소스를 통해 컴파일할 수도 있습니다.즉, unit1.c와 unit2.c를 컴파일한 후 두 개체를 링크하는 대신 all.c를 컴파일하여 #unit1.c와 unit2.c를 포함시킵니다.따라서 모든 컴파일러 최적화의 이점을 얻을 수 있습니다.

이것은 헤더만을 C++로 쓰는 것과 매우 비슷합니다(또한 C에서는 더 쉽게 할 수 있습니다).

이 기법은 프로그램을 처음부터 사용할 수 있도록 작성하면 충분하지만 C 시멘틱의 일부가 변경되어 정적 변수나 매크로 충돌과 같은 몇 가지 문제에 직면할 수 있다는 것도 알아야 합니다.대부분의 프로그램에서는 발생하는 작은 문제를 쉽게 극복할 수 있습니다.또, 독자적인 소스로의 컴파일은 매우 느리고, 대량의 메모리를 필요로 하는 경우가 있습니다(통상, 최신의 시스템에서는 문제가 되지 않습니다).

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

register 키워드와 마찬가지로 이 트릭도 곧 폐지될 가능성이 있습니다.링커를 통한 최적화는 컴파일러 gcc: 링크 시간 최적화에서 지원됩니다.

루프 내의 원자성 태스크 분리

이게 더 까다로워.알고리즘 설계와 옵티마이저가 캐시를 관리하고 할당을 등록하는 방법 간의 상호작용에 관한 것입니다.대부분의 경우 프로그램은 일부 데이터 구조를 루프하고 각 항목에 대해 몇 가지 작업을 수행해야 합니다.수행되는 작업은 논리적으로 독립적인 두 작업 간에 분할될 수 있습니다.이 경우 동일한 경계에 두 개의 루프를 사용하여 동일한 프로그램을 작성하여 정확히 하나의 작업을 수행할 수 있습니다.경우에 따라서는 이 방법으로 쓰는 것이 고유 루프보다 빠를 수 있습니다(자세한 내용은 더 복잡합니다).그러나 간단한 태스크 케이스에서는 모든 변수를 프로세서 레지스터에 저장할 수 없고 더 복잡한 경우에는 일부 레지스터를 메모리에 쓰고 나중에 다시 읽어야 하며 추가 비용보다 비용이 더 높습니다.nal 흐름 제어).

이 기능(이 기술을 사용하는 프로파일 퍼포먼스 유무)은 레지스터를 사용하는 경우와 마찬가지로 개선된 기능보다 성능이 저하될 수 있습니다.

옵티마이저는 프로그램의 성능을 실제로 제어하는 것이 아니라, 사용자가 제어합니다.적절한 알고리즘과 구조 및 프로파일, 프로파일, 프로파일을 사용합니다.

즉, 작은 함수의 내부 루프를 다른 파일의 내부 루프로 하면 인라인을 할 수 없게 됩니다.

가능하면 변수 주소를 사용하지 마십시오.포인터를 요구하는 것은 변수를 메모리에 보관해야 한다는 것을 의미하기 때문에 "자유"가 아닙니다.포인터를 피하면 어레이도 레지스터에 저장할 수 있습니다.이것은 벡터화에 불가결합니다.

다음 포인트로 넘어가면 ^#$@ 매뉴얼을 읽어주세요!GCC는 플레인 C 코드를 벡터화할 수 있습니다.__restrict__와 여기__attribute__( __aligned__ )만약 옵티마이저에서 아주 구체적인 것을 원한다면, 당신은 구체화해야 할 수도 있습니다.

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

에일리어스: Load-Hit-Store는 꽉 막힌 루프에서 파괴적일 수 있습니다.어떤 메모리 위치를 읽고 다른 메모리 위치에 쓰면서 그것들이 분리된 것을 알고 있다면 함수 파라미터에 alias 키워드를 주의 깊게 배치하면 컴파일러가 더 빠른 코드를 생성할 수 있습니다.그러나 메모리 영역이 중복되고 '에일리어스'를 사용했다면 정의되지 않은 동작의 디버깅 세션이 시작됩니다.

캐시 미스:대부분 알고리즘이기 때문에 컴파일러를 어떻게 도울 수 있을지는 잘 모르겠지만 메모리를 프리페치하기 위한 본질적인 기능이 있습니다.

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

사람들이 쓰는 코드의 대부분은 I/O(지난 30년 동안 돈을 벌기 위해 쓴 모든 코드)이기 때문에 대부분의 사람들을 위한 옵티마이저의 활동은 학술적인 것이 될 것입니다.

그러나 코드를 최적화하려면 컴파일러에게 최적화하도록 지시해야 합니다.많은 사람들이 C++ 벤치마크를 여기에 올립니다.이것은 옵티마이저를 유효하게 하지 않으면 의미가 없습니다.

코드에 가능한 한 const correctness를 사용합니다.이를 통해 컴파일러는 훨씬 더 효율적으로 최적화할 수 있습니다.

이 문서에서는 기타 최적화 힌트에 대해 설명합니다.CPP 최적화(단, 조금 오래된 문서)

특징:

  • 생성자 초기화 목록 사용
  • 프리픽스 연산자를 사용하다
  • 명시적 생성자 사용
  • 인라인 함수
  • 일시적인 것을 피하다
  • 가상 기능의 비용을 알고 있다
  • 참조 매개 변수를 통해 개체 반환
  • 클래스별 할당을 고려하다
  • stl 컨테이너 할당자 고려
  • 빈 멤버의 최적화'
  • 기타

최적화 C 컴파일러를 작성했습니다.여기서 고려해야 할 매우 유용한 사항을 몇 가지 소개합니다.

  1. 대부분의 기능을 정적으로 합니다.이것에 의해, 프로시저간의 상수 전파와 에일리어스 분석이 기능할 수 있게 됩니다.그렇지 않으면 컴파일러는 함수를 변환 유닛 외부에서 호출할 수 있고 파라미터에 대해 전혀 알려지지 않은 값이 있다고 가정할 필요가 있습니다.잘 알려진 오픈 소스 라이브러리를 보면, 실제로 외부여야 하는 것 외에는 모두 함수에 정적인 마크를 붙입니다.

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

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

  4. 포인터 파라미터는 필요한 경우에만 사용하고 가능하면 제한으로 표시합니다.이것은 프로그래머가 에일리어스가 없음을 보증하기 때문에 에일리어스 분석에 큰 도움이 됩니다(프로시저간 에일리어스 분석은 보통 매우 원시적입니다).매우 작은 구조 객체는 참조가 아닌 값으로 전달해야 합니다.

  5. 가능한 한 포인터 대신 배열을 사용하십시오. 특히 내부 루프(a[i])는 더욱 그렇습니다.배열은 보통 에일리어스 분석에 대한 자세한 정보를 제공하며, 일부 최적화 후에는 동일한 코드가 생성됩니다(궁금할 경우 루프 강도 감소 검색).이로 인해 루프 불변 코드모션이 적용될 가능성도 높아집니다.

  6. (현재 루프 반복에 의존하지 않음) 큰 기능 또는 외부 기능에 루프 호출을 외부에 호이스트합니다.작은 함수는 많은 경우에 인라인되거나 상승하기 쉬운 내장 함수로 변환되지만, 큰 함수는 컴파일러가 실제로 그렇지 않을 때 부작용이 있는 것처럼 보일 수 있습니다.일부 컴파일러에 의해 모델링된 표준 라이브러리의 일부 함수를 제외하고 외부 함수에 대한 부작용은 전혀 알려져 있지 않으므로 루프 불변 코드 모션이 가능합니다.

  7. 여러 조건을 가진 테스트를 작성할 경우 가장 가능성이 높은 테스트를 먼저 실시합니다.if ( a | | b | | c )는 b가 다른 테스트보다 더 참일 가능성이 높은 경우 if ( b | a | c)가 되어야 합니다.컴파일러는 일반적으로 조건의 가능한 값과 어떤 브랜치를 더 많이 사용하는지 전혀 알지 못합니다(프로파일 정보를 사용하여 알 수 있지만 이를 사용하는 프로그래머는 거의 없습니다).

  8. 스위치를 사용하는 것이 if(a || b || ...)와 같은 테스트를 실행하는 것보다 빠릅니다.|| z) 먼저 컴파일러가 자동으로 실행되는지, 어떤 컴파일러는 자동으로 실행되는지, 어떤 컴파일러는 자동으로 실행되는지, 그리고 if를 사용하는 것이 더 읽기 쉬운지를 확인합니다.

바보같은 작은 팁이지만 아주 적은 양의 속도와 코드를 절약할 수 있는 팁입니다.

함수 인수는 항상 같은 순서로 전달합니다.

f_2를 호출하는 f_1(x, y, z)이 있는 경우 f_2를 f_2(x, y, z)로 선언합니다.f_2(x, z, y)로 선언하지 마십시오.

그 이유는 C/C++ 플랫폼 ABI(AKA 호출 규칙)가 특정 레지스터 및 스택로케이션에서 인수를 전달한다고 약속하기 때문입니다.인수가 이미 올바른 레지스터에 있는 경우 인수는 이동할 필요가 없습니다.

분해된 코드를 읽는 동안 나는 사람들이 이 규칙을 따르지 않았기 때문에 우스꽝스러운 레지스터가 뒤틀리는 것을 보았다.

실제로 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();
}

싱글톤 getter의 경우 콜은 그다지 비싸지 않을 수 있지만, 확실히 비용이 듭니다(일반적으로 오브젝트가 작성되었는지 여부를 확인하고 작성되지 않은 경우 오브젝트를 작성한 후 반환).더 복잡해질수록 시간이 낭비될 거야

  1. 모든 변수 선언에는 가능한 한 로컬 스코프를 사용합니다.

  2. const 수 있으면 언제든지

  3. 등록 유무에 관계없이 프로파일을 작성할 계획이 없는 한 레지스터를 사용하지 마십시오.

이들 중 처음 2개, 특히 #1은 옵티마이저가 코드를 분석하는 데 도움이 됩니다.특히 레지스터에 보관할 변수를 잘 선택하는 데 도움이 됩니다.

register 키워드를 맹목적으로 사용하는 것은 최적화에 도움이 될 수 있습니다.어셈블리의 출력이나 프로파일을 보기 전에는 무엇이 중요한지 알 수 없습니다.

코드에서 뛰어난 성능을 얻기 위해서는 캐시의 일관성을 최대화할 수 있도록 데이터 구조를 설계해야 합니다.하지만 질문은 옵티마이저에 관한 것이었다.

데이터를 네이티브/내추럴한 경계에 맞춥니다.

한 번 접한 적이 있는데, 단순히 메모리가 부족하다는 증상이 나타났지만 그 결과 성능이 크게 향상되고 메모리 사용 공간이 대폭 감소했습니다.

이 경우 문제는 사용하고 있는 소프트웨어가 할당량이 적다는 것입니다.여기에는 4바이트, 저기에는 6바이트를 할당하는 것 등이 있습니다.8~12바이트 범위에서 실행되는 작은 객체도 많습니다.문제는 프로그램이 작은 것을 많이 필요로 하는 것이 아니라 작은 것을 개별적으로 많이 할당했기 때문에 (이 특정 플랫폼에서) 각 할당량이 32바이트로 늘어났다는 것입니다.

솔루션의 일부는 알렉산드레스쿠 스타일의 작은 객체 풀을 조립하는 것이었지만 개별 항목뿐만 아니라 작은 객체의 배열을 할당할 수 있도록 확장했습니다.캐시에 한 번에 더 많은 항목이 들어가므로 성능에도 큰 도움이 되었습니다.

이 솔루션의 다른 부분은 수동 관리 char* 멤버의 빈번한 사용을 SSO(소문자열 최적화) 문자열로 대체하는 것이었습니다.최소 할당량은 32바이트입니다.문자* 뒤에 28자 버퍼가 내장되어 있는 문자열 클래스를 구축했습니다.따라서 문자열의 95%는 추가 할당이 필요하지 않습니다(그리고 이 라이브러리의 거의 모든 문자*를 수동으로 이 새로운 클래스로 대체했습니다.재밌든 아니든 간에).이는 메모리 단편화에도 큰 도움이 되었고, 그 후 다른 포인팅 오브젝트에 대한 참조 인접성을 증가시켰고, 마찬가지로 성능도 향상되었습니다.

답변에 대한 @MSALTERs 코멘트로부터 배운 깔끔한 기술을 통해 컴파일러는 어떤 조건에 따라 다른 오브젝트를 반환하더라도 복사 삭제를 수행할 수 있습니다.

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

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

반복 호출하는 작은 함수가 있는 경우, 과거에는 "static inline"으로 헤더에 포함시킴으로써 큰 이득을 얻었습니다.ix86의 함수 호출은 놀라울 정도로 비쌉니다.

명시적 스택을 사용하여 반복적이지 않은 방법으로 재귀 함수를 재실장하는 것도 많은 이득을 얻을 수 있지만, 그러면 실제로 개발 시간과 이득의 영역에 있게 됩니다.

최적화에 대한 두 번째 조언입니다.제 첫 번째 조언과 마찬가지로 이것은 일반적인 목적이며 언어나 프로세서 고유의 것이 아닙니다.

컴파일러의 메뉴얼을 충분히 읽어, 그 내용을 이해한다.컴파일러를 최대한 활용하세요.

적절한 알고리즘을 선택하는 것이 프로그램의 성능을 끌어내는 데 중요하다고 생각하는 다른 응답자 중 한 두 명에게 동의합니다.게다가 컴파일러의 사용에의 투자 회수율(코드 실행의 개선으로 측정)은, 코드 조정의 회수율보다 훨씬 높다.

네, 컴파일러 라이터는 거대 부호화 업체 출신이 아닙니다.또한 매뉴얼이나 컴파일러 이론에 따르면 컴파일러에는 오류가 포함되어 있습니다.또한 컴파일러의 속도가 빨라지면 속도가 느려질 수 있습니다.그렇기 때문에 한 번에 한 단계씩 진행하여 테스트 전과 테스트 후의 성능을 측정해야 합니다.

그리고 최종적으로는 컴파일러 플래그의 조합에 직면할 가능성이 있기 때문에 다양한 컴파일러 플래그를 사용하여 make를 실행하고 대규모 클러스터에서 작업을 큐잉하여 런타임 통계를 수집하기 위한 스크립트가 필요합니다.PC에 사용자와 Visual Studio만 있다면 충분한 컴파일러 플래그의 조합을 시도하기 훨씬 전에 흥미를 잃게 될 것입니다.

안부 전해요

마크.

코드를 처음 집어들었을 때 컴파일러 플래그를 만지작거리면 보통 1.4~2.0배의 퍼포먼스를 얻을 수 있습니다(새로운 버전의 코드는 이전 버전의 1/1.4 또는 1/2로 실행됩니다).그렇다. 그것은 아마도 내가 작업하는 코드의 많은 부분을 발명한 과학자들 사이에 컴파일러 지식이 부족하다는 지적일 것이다. 내 우수성의 증상이라기 보다는.컴파일러 플래그를 max로 설정했을 경우(또한 -O3만 있는 경우는 거의 없습니다), 1.05 또는 1.1의 다른 계수를 얻기 위해서는 수개월이 걸릴 수 있습니다.

DEC가 알파 프로세서와 함께 나왔을 때, 컴파일러는 항상 레지스터에 최대 6개의 인수를 자동으로 넣으려고 하기 때문에 함수에 대한 인수 수를 7개 이하로 유지하는 것이 권장되었습니다.

퍼포먼스를 확보하기 위해 우선 유지보수 가능한 코드(컴포넌트화, 느슨한 결합 등) 작성에 집중합니다.이를 통해 개서, 최적화 또는 단순한 프로파일링을 위해 부품을 분리해야 할 경우 큰 수고를 들이지 않아도 됩니다.

Optimizer는 프로그램 성능을 약간 향상시킵니다.

여기에서는 좋은 답변을 얻을 수 있지만, 그들은 당신의 프로그램이 처음부터 매우 최적이라고 가정하고, 당신은 이렇게 말합니다.

프로그램이 올바르게 작성되고 완전히 최적화되어 컴파일되고 테스트되어 실제 가동에 투입되었다고 가정합니다.

내 경험으로는 프로그램이 올바르게 작성되어 있을지는 모르지만, 그것이 거의 최적이라고는 할 수 없다.거기까지는 손이 많이 간다.

예를 들면, 이 답변은, 매크로 최적화에 의해서, 지극히 합리적으로 보이는 프로그램이 어떻게 40배 이상 빨리 만들어졌는지를 나타내고 있습니다.고속화는 처음 작성된 처럼 모든 프로그램에서 실행할 수 있는 것은 아니지만, 많은 프로그램(매우 작은 프로그램 제외)에서 가능합니다.

그 후, (핫 스팟의) 마이크로 최적화를 실시하면, 큰 메리트를 얻을 수 있습니다.

인텔 컴파일러를 사용합니다.Windows 와 Linux 의 양쪽 모두에서.

어느 정도 완료되면 코드를 프로파일합니다.그런 다음 핫스팟에 접속하여 코드를 변경하여 컴파일러가 더 나은 작업을 수행할 수 있도록 합니다.

코드가 계산 코드이고 루프가 많은 경우 - 인텔 컴파일러의 벡터화 리포트는 매우 유용합니다. - 도움이 되는 'vec-report'를 찾아주세요.

따라서 주요 아이디어인 성능 중요 코드를 다듬고, 나머지 우선순위는 정확하고 유지보수가 가능한 짧은 기능, 1년 후에 이해할 수 있는 명확한 코드입니다.

C++에서 사용한 최적화 중 하나는 아무것도 하지 않는 생성자를 만드는 것입니다.오브젝트를 동작 상태로 만들려면 init()를 수동으로 호출해야 합니다.

이것은 이러한 클래스의 큰 벡터가 필요한 경우에 도움이 됩니다.

벡터 공간을 할당하기 위해 reserve()를 호출하지만 컨스트럭터는 실제로 오브젝트가 있는 메모리 페이지를 터치하지 않습니다.그래서 주소 공간을 좀 썼지만 실제로 많은 물리 메모리를 소비하지는 않았습니다.관련 공사비와 관련된 페이지 폴트를 회피합니다.

벡터를 채울 객체를 생성할 때 init()를 사용하여 설정합니다.이것에 의해, 페이지 폴트의 합계가 제한되기 때문에, 벡터를 채울 때에 벡터의 사이즈를 변경할 필요가 없어집니다.

제가 한 일은 사용자가 프로그램이 조금 지연될 것으로 예상할 수 있는 장소에 비용이 많이 드는 액션을 유지하려고 한 것입니다.전체적인 퍼포먼스는 응답성과 관련이 있지만 완전히 동일하지는 않습니다.또한 퍼포먼스의 중요한 부분은 응답성입니다.

지난 번에는 전체적인 성능을 개선해야 할 때 최적의 알고리즘이 없는지 주의하면서 캐시에 문제가 있을 가능성이 높은 곳을 찾아다녔습니다.처음에 프로파일을 작성하고 성능을 측정했으며 매번 변경 후 다시 측정했습니다.그 후 회사는 무너졌지만, 어쨌든 흥미롭고 유익한 일이었습니다.

어레이가 2의 거듭제곱을 가지도록 엘리먼트 수로 선언하면 옵티마이저는 개별 엘리먼트를 검색할 때 곱셈을 비트 수로 대체하여 강도를 낮출 수 있다고 오랫동안 생각해 왔습니다.

소스 파일의 맨 위에 작은 함수나 자주 호출되는 함수를 배치합니다.이것에 의해, 컴파일러는 인라이닝의 기회를 찾기 쉬워집니다.

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

반응형