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


116

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

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

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

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

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

다음은 관련 질문입니다.

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

[편집] 오프셋 관련 링크


7
이 (재미있는) 질문에 '하나의'확실한 대답은 ...이 없기 때문에 커뮤니티 위키 이럴위한 좋은 후보가 될 수
ChristopheD

매번 그리워요. 지적 해주셔서 감사합니다.
EvilTeach

'더 좋다'는 것은 단순히 '더 빠르다'는 뜻입니까, 아니면 다른 우수성 기준을 염두에두고 있습니까?
High Performance Mark

1
특히 이식성이 좋은 좋은 레지스터 할당자를 작성하는 것은 매우 어렵고 레지스터 할당은 성능과 코드 크기에 절대적으로 필수적입니다. register실제로 성능에 민감한 코드는 열악한 컴파일러와 싸워서 더 이식 가능하게 만들었습니다.
Potatoswatter

1
@EvilTeach : 커뮤니티 위키는 "정답이 없음"을 의미하지 않으며 주관적인 태그와 동의어가 아닙니다. 커뮤니티 위키는 다른 사람들이 편집 할 수 있도록 게시물을 커뮤니티에 제출하고자 함을 의미합니다. 마음에 들지 않더라도 질문을 위키로 작성해야한다는 부담감을 느끼지 마십시오.
Juliet

답변:


54

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

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이이 스레드에서 언급 함)를 피하는 데 도움이 될 수 있습니다.


7
이것은 프로그래머가 쉽게 읽고 이해할 수 있도록 만드는 추가적인 이점이 있습니다. 그 이유는 분명하지 않은 부작용에 대해 걱정할 필요가 없기 때문입니다.
Michael Burr

대부분의 IDE는 기본적으로 로컬 변수를 표시하므로 입력이 적습니다.
EvilTeach

9
제한된 포인터를 사용하여 최적화를 활성화 할 수도 있습니다
Ben Voigt

4
@Ben-사실이지만이 방법이 더 명확하다고 생각합니다. 또한 입력과 출력이 겹치면 결과가 제한된 포인터로 지정되지 않은 것으로 생각합니다 (아마도 디버그와 릴리스 사이에 다른 동작을 얻음),이 방법은 적어도 일관성이 있습니다. 오해하지 마세요. 저는 제한을 사용하는 것을 좋아하지만 더 필요하지 않은 것을 좋아합니다.
celion

Foo가 몇 메가의 데이터를 복사하는 복사 작업을 정의하지 않기를 바랄뿐입니다 ;-)
Skizz

76

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

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

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

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

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


20
Compiier 개발자는 다른 사람들과 마찬가지로 유한 한 시간을 가지고 있습니다. 모든 최적화가 컴파일러에 적용되는 것은 아닙니다. 마찬가지로 &%(적, 최적화,하지만 성능이 크게 영향을 미칠 수있는 경우 드물게) 두 가지의 힘을합니다. 성능에 대한 트릭을 읽는 경우 작동 여부를 알 수있는 유일한 방법은 변경을 수행하고 영향을 측정하는 것입니다. 컴파일러가 당신을 위해 무언가를 최적화 할 것이라고 가정하지 마십시오.
Dave Jarvis

22
& 및 %는 대부분의 다른 저렴한 무료 산술 트릭과 함께 거의 항상 최적화되어 있습니다. 최적화되지 않은 것은 오른쪽 피연산자가 항상 2의 거듭 제곱이되는 변수 인 경우입니다.
Potatoswatter

8
명확히하기 위해 일부 독자를 혼란스럽게하는 것 같습니다. 제가 제안하는 코딩 실습의 조언은 먼저 성능 기준을 설정하기 위해 메모리 레이아웃 명령을 사용하지 않는 간단한 코드를 개발하는 것입니다. 그런 다음 한 번에 하나씩 시도하고 그 영향을 측정하십시오. 나는 작업 수행에 대한 조언을 제공하지 않았습니다.
High Performance Mark

17
일정한 전원의 명의를 들어 n, GCC의을 대체 % n& (n-1) 최적화를 사용할 경우에도 . 그것은 정확히 "아주 드물게"가 아닙니다. ...
Porculus

12
음의 정수 나눗셈에 대한 C의 멍청한 규칙 때문에 % 는 &로 최적화 할 수 없습니다 (반올림하고 항상 양의 나머지를 갖는 대신 0으로 반올림하고 음의 나머지를 가짐). 그리고 대부분의 경우 무지한 코더는 서명 된 유형을 사용합니다 ...
R .. GitHub STOP HELPING ICE

47

메모리를 순회하는 순서는 성능에 큰 영향을 미칠 수 있으며 컴파일러는 그것을 파악하고 수정하는 데 실제로 좋지 않습니다. 성능에 관심이 있다면 코드를 작성할 때 캐시 지역성 문제를 양심적으로 고려해야합니다. 예를 들어 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];
  }
}

엄밀히 말하면 이것은 최적화 문제는 아니지만 최적화 문제입니다.
EvilTeach

10
물론 최적화 문제입니다. 사람들은 수십 년 동안 자동 루프 교환 최적화에 대한 논문을 작성해 왔습니다.
Phil Miller

20
@Potatoswatter 무슨 소리 야? C 컴파일러는 동일한 최종 결과가 관찰되는 한 원하는 모든 작업을 수행 할 수 있으며 실제로 GCC 4.4에는 -floop-interchange최적화 프로그램이 수익성이 있다고 판단되면 내부 및 외부 루프가 뒤집힐 것입니다.
ephemient

2
허, 잘 됐네요. C 의미 체계는 종종 앨리어싱 문제로 인해 손상됩니다. 여기서 진짜 조언은 그 깃발을 전달하는 것입니다!
Potatoswatter 2010 년

36

일반 최적화

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

작은 함수를 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접두사 를 사용하여 해결할 수 있습니다 .

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

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


2
흥미롭게도 큰 문장 대신 몇 개의 작은 문장으로 더 나은 코드를 얻은 예를 제공 할 수 있습니다. 부울을 사용하여 if를 다시 작성하는 예를 보여줄 수 있습니까? 일반적으로 캐시 크기에 대한 느낌이 더 좋기 때문에 루프를 컴파일러에 풀어 놓은 상태로 둡니다. 나는 sprintfing, fwriting이라는 아이디어에 약간 놀랐습니다. 나는 fprintf가 실제로 그렇게하는 것이라고 생각합니다. 여기서 좀 더 자세히 설명해 주시겠습니까?
EvilTeach

1
fprintf별도의 버퍼로 포맷 한 다음 버퍼를 출력 한다는 보장은 없습니다 . 간소화 된 (메모리 사용을 위해) fprintf형식이 지정되지 않은 모든 텍스트를 출력 한 다음 형식화 및 출력하고 전체 형식 문자열이 처리 될 때까지 반복하여 각 출력 유형 (형식화 됨 대 형식화되지 않음)에 대해 1 개의 출력 호출을 만듭니다. 다른 구현에서는 전체 새 문자열을 보유하기 위해 각 호출에 대해 동적으로 메모리를 할당해야합니다 (임베디드 시스템 환경에서는 좋지 않음). 내 제안은 출력 수를 줄입니다.
Thomas Matthews

3
한 번 루프를 롤업하여 상당한 성능 향상을 얻었습니다. 그런 다음 간접적 인 방법을 사용하여 더 세게 롤업하는 방법을 알아 냈고 프로그램이 눈에 띄게 빨라졌습니다. (프로파일 링은이 특정 기능이 런타임의 60-80 % 인 것으로 나타 났으며, 전후에 성능을주의 깊게 테스트했습니다.) 개선이 더 나은 지역성 때문이라고 생각하지만 완전히 확신 할 수는 없습니다.
David Thornley 2010-04-02

16
이들 중 다수는 프로그래머가 컴파일러를 최적화하는 데 도움이되는 방법이 아니라 프로그래머 최적화이며, 이는 원래 질문의 요지였습니다. 예를 들어, 루프 풀기. 예, 직접 언 롤링을 수행 할 수 있지만 컴파일러가 언 롤링하는 데 어떤 장애물이 있는지 파악하고이를 제거하는 것이 더 흥미 롭다고 생각합니다.
Adrian McCarthy

26

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

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

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

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


14
이것은 좋은 대답이지만 전체 프로그램 최적화가 점점 더 대중화되고 있으며 실제로 번역 단위에서 함수를 인라인 할 수 있습니다.
Phil Miller

1
@Novelocrat 네-말할 필요도없이 내가 처음으로 A.c인라인되는 것을 보았을 때 매우 놀랐 습니다 B.c.
Jonathon Reinhart 2013 년

18

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

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

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

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


4
로드 적중 저장소 및 다른 레지스터 유형의 경우 +1. x86에서 얼마나 큰 거래인지 잘 모르겠지만, 그들은 PowerPC (예 : Xbox360 및 Playstation3)에 대해 비방하고 있습니다.
celion

컴파일러 루프 최적화 기술에 대한 대부분의 논문은 완벽한 중첩을 가정합니다. 즉, 가장 안쪽을 제외한 각 루프의 본문은 또 다른 루프 일뿐입니다. 이 문서는 일반화 할 수 있다는 것이 매우 분명하더라도이를 일반화하는 데 필요한 단계를 논의하지 않습니다. 따라서 추가 노력이 필요하기 때문에 많은 구현이 실제로 이러한 일반화를 지원하지 않을 것으로 예상합니다. 따라서 루프에서 캐시 사용을 최적화하는 많은 알고리즘이 불완전한 중첩보다 완벽한 중첩에서 훨씬 더 잘 작동 할 수 있습니다.
Phil Miller

11

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

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


7
나는 독특하다고 고백합니다. 나는 메모리 대역폭이 제한된 대규모 과학적 숫자 처리 코드를 작업합니다. 일반적인 프로그램 인구의 경우 Neil에 동의합니다.
High Performance Mark

6
진실; 하지만 요즘에는 I / O 바운드 코드의 상당수 가 실질적으로 비관적 인 언어로 작성됩니다 . 컴파일러도없는 언어입니다. C와 C ++가 여전히 사용되는 영역은 무언가를 최적화하는 것이 더 중요한 영역 (CPU 사용량, 메모리 사용량, 코드 크기 ...)
것이라고 생각

3
저는 지난 30 년 동안 I / O가 거의없는 코드 작업에 대부분을 보냈습니다. 데이터베이스를 사용하여 2 년 동안 저장하십시오. 그래픽, 제어 시스템, 시뮬레이션-I / O 바운드 없음. I / O가 대부분의 사람들에게 병목 현상이 있었다면 인텔과 AMD에 많은 관심을 기울이지 않을 것입니다.
phkahler 2010

2
예, 저는이 주장을 실제로 사지 않습니다. 그렇지 않으면 (제 직업에서) I / O를 수행하면서 컴퓨팅 시간을 더 많이 소비하는 방법을 찾지 않을 것입니다. 또한 내가 본 I / O 바인딩 소프트웨어의 대부분은 I / O가 느리게 수행 되었기 때문에 I / O 바인딩되었습니다. 액세스 패턴을 최적화하면 (메모리와 마찬가지로) 성능이 크게 향상 될 수 있습니다.
dash-tom-bang

3
저는 최근에 C ++ 언어로 작성된 코드가 I / O 바인딩이 거의 없다는 것을 발견했습니다. 물론 대량 디스크 전송을 위해 OS 함수를 호출하는 경우 스레드가 I / O 대기 상태가 될 수 있습니다 (하지만 캐싱을 사용하는 경우에도 문제가 있음). 그러나 일반적인 I / O 라이브러리 기능은 표준이고 이식성이 있기 때문에 모두가 권장하는 기능은 실제로 현대 디스크 기술 (저렴한 가격에도 불구하고)에 비해 비참하게 느립니다. 대부분의 경우 I / O는 몇 바이트 만 쓴 후 디스크까지 플러시하는 경우에만 병목 현상이 발생합니다. OTOH, UI는 다른 문제입니다. 우리 인간은 느립니다.
Ben Voigt

11

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

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

하이라이트:

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

8
별로, 드물게. 그러나 실제 정확성은 향상됩니다.
Potatoswatter

5
C 및 C ++에서 컴파일러는 const를 사용하여 최적화 할 수 없습니다. 캐스트는 잘 정의 된 동작이기 때문입니다.
dsimcha

+1 : const는 컴파일 된 코드에 직접적으로 영향을 미치는 좋은 예입니다. @dsimcha의 의견 다시-좋은 컴파일러는 이런 일이 발생하는지 테스트합니다. 물론 좋은 컴파일러는 어쨌든 그렇게 선언되지 않은 const 요소를 "찾을"것입니다 ...
Hogan

@dsimcha : 변화하는 const restrict 자격을 갖춘 포인터를하지만, 정의되어 있지 않습니다. 따라서 컴파일러는 이러한 경우에 다르게 최적화 할 수 있습니다.
Dietrich Epp 2011-06-26

6
비 객체 const에 대한 const참조 또는 const포인터 에서 @dsimcha 캐스팅 const은 잘 정의되어 있습니다. 실제 const객체 (즉, const원래 선언 된 객체)를 수정하는 것은 아닙니다.
스티븐 린

9

가능한 한 정적 단일 할당을 사용하여 프로그래밍을 시도합니다. 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;
}

물론 리턴 포인트가 하나 뿐인 경우 논리를 뒤집을 수 있습니다.

(나중에 기억했던 트릭)

가능한 경우 함수를 정적으로 선언하는 것은 항상 좋은 생각입니다. 컴파일러가 특정 함수의 모든 호출자를 설명했다는 것을 스스로 증명할 수 있다면 최적화라는 이름으로 해당 함수에 대한 호출 규칙을 위반할 수 있습니다. 컴파일러는 종종 함수를 호출하는 함수가 일반적으로 해당 매개 변수가있을 것으로 예상하는 레지스터 또는 스택 위치로 매개 변수를 이동하는 것을 피할 수 있습니다 (이 작업을 수행하려면 호출 된 함수와 모든 호출자의 위치 모두에서 이탈해야합니다). 컴파일러는 종종 호출 된 함수에 필요한 메모리와 레지스터를 알고 호출 된 함수가 방해하지 않는 레지스터 또는 메모리 위치에있는 변수 값을 보존하기위한 코드 생성을 피할 수 있습니다. 이것은 함수에 대한 호출이 거의 없을 때 특히 잘 작동합니다.


2
범위, LLVM, GCC 및 내 컴파일러를 테스트 할 때 실제로 빼기를 사용할 필요는 없습니다. 뺄셈이있는 코드가 무엇을하는지 이해하는 사람은 거의 없으며 실제로 작동하는 이유는 더 적습니다.
Gratian Lup

위의 예에서 b ()는 호출 할 수 없습니다. (x <0)이면 a ()가 호출되기 때문입니다.
EvilTeach

@EvilTeach 아니에요. a () 호출 결과 비교는! x
nategoose

@nategoose. x가 -3이면! x는 참입니다.
EvilTeach 2018

그래서 -3 그래서, 사실, @EvilTeach에서 C 0은 거짓이며, 다른 모든 사실 -3 거짓!
nategoose

9

최적화 C 컴파일러를 작성했으며 여기에 고려해야 할 매우 유용한 몇 가지 사항이 있습니다.

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

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

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

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

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

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

  7. 여러 조건으로 테스트를 작성할 때 가장 가능성이 높은 것을 먼저 배치하십시오. b 가 다른 것보다 참일 가능성이 더 높으면 if (a || b || c)는 if (b || a || c) 여야합니다. 컴파일러는 일반적으로 조건의 가능한 값과 더 많이 사용되는 분기에 대해 아무것도 모릅니다 (프로필 정보를 사용하여 알 수 있지만이를 사용하는 프로그래머는 거의 없음).

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


7

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

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


내 경험 법칙은 동적으로 할당해야하는 경우 배열을 가져 와서 다시 할 필요가 없다는 것입니다. 벡터를 미리 할당하십시오.
EvilTeach

7

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

항상 동일한 순서로 함수 인수를 전달하십시오.

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

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

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


2
C도 C ++도 특정 레지스터 나 스택 위치를 전달하는 것에 대해 보증하거나 언급하지도 않습니다. 그것은이다 ABI 매개 변수 전달의 세부 사항을 결정한다 (예를 들어 리눅스 ELF).
Emmet

5

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

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

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

그러나 프로그램을 잘 설계하면 고유 한 공통 소스를 통해 컴파일 할 수도 있습니다. 즉, unit1.c와 unit2.c를 컴파일하는 대신 두 개체를 모두 연결하고 #include unit1.c 및 unit2.c 만있는 all.c를 컴파일합니다. 따라서 모든 컴파일러 최적화의 이점을 누릴 수 있습니다.

C ++로 헤더 만 작성하는 것과 매우 유사합니다 (C에서 더 쉽게 수행 할 수 있음).

이 기술은 처음부터 활성화하도록 프로그램을 작성하는 경우 충분히 쉽지만 C 의미 체계의 일부를 변경하고 정적 변수 또는 매크로 충돌과 같은 몇 가지 문제를 해결할 수 있음을 인식해야합니다. 대부분의 프로그램에서 발생하는 작은 문제를 쉽게 극복 할 수 있습니다. 또한 고유 한 소스로 컴파일하는 것이 훨씬 느리고 엄청난 양의 메모리를 차지할 수 있습니다 (일반적으로 최신 시스템에서는 문제가되지 않음).

이 간단한 기술을 사용하여 제가 작성한 일부 프로그램을 10 배 더 빠르게 만들었습니다!

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

루프에서 원자 작업 분리

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

레지스터를 사용하는 것과 같이 향상된 성능보다 성능이 떨어질 수 있으므로이 트릭을 사용하는 프로파일 성능 여부에주의하십시오.


2
예, 지금까지 LTO는이 게시물의 전반부를 중복되고 아마도 나쁜 조언으로 만들었습니다.
underscore_d

@underscore_d : 여전히 몇 가지 문제가 있지만 (주로 내 보낸 기호의 가시성과 관련됨) 단순한 성능 관점에서 더 이상 문제가 없을 것입니다.
한국 표준 과학 연구원

4

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


5
사용되는 함수를 소스에 물리적으로 가깝게 배치하면 개체 파일에서 서로 가깝고 실행 파일에서 서로 가깝게 될 가능성이 높아집니다. 이렇게 향상된 명령어 위치는 실행 중 명령어 캐시 누락을 방지하는 데 도움이됩니다.
paxos1977

AIX 컴파일러에는 해당 동작을 권장하는 컴파일러 스위치가 있습니다. -qipa [= <suboptions_list>] | -qnoipa IPA (프로 시저 간 분석)로 알려진 최적화 클래스를 설정하거나 사용자 정의합니다.
EvilTeach

4
이것이 필요하지 않은 개발 방법을 갖는 것이 가장 좋습니다. 이 사실을 비 모듈 식 코드를 작성하기위한 핑계로 사용하면 전체적으로 느리고 유지 관리 문제가있는 코드가 생성됩니다.
Hogan

3
이 정보는 약간 오래된 것 같습니다. 이론적으로 현재 많은 컴파일러에 내장 된 전체 프로그램 최적화 기능 (예 : gcc의 "링크 시간 최적화")은 동일한 이점을 제공하지만 완전히 표준 워크 플로 (모든 파일을 하나의 파일에 넣는 것보다 더 빠른 재 컴파일 시간 포함) !)
Ponkadoodle 2014

@Wallacoloo 확실히, 이것은 faaar outta date입니다. FWIW, 저는 오늘 처음으로 GCC의 LTO를 사용했으며 다른 모든 것은 동일 -O3합니다. 제 프로그램에서 원래 크기의 22 %를 초과 했습니다. (CPU 바운드가 아니기 때문에 속도에 대해별로 할 말이 없습니다.)
underscore_d

4

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

예:

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를 사용하는 데 필요한 요구 사항은 테일 엔드 재귀를 통해 최적화 할 수 있도록 특별히 설계되었습니다.


1
질문은 C에 관한 것입니다. C는 꼬리 재귀를 제거하지 않으므로 꼬리 또는 기타 재귀, 재귀가 너무 깊어지면 스택이 날아갈 수 있습니다.
Toad

1
goto를 사용하여 호출 규칙 문제를 피했습니다. 그렇게하면 오버 헤드가 적습니다.
EvilTeach

2
@hogan : 이것은 나에게 새로운 것입니다. 이것을 수행하는 컴파일러를 가리킬 수 있습니까? 그리고 그것이 실제로 그것을 최적화하는지 어떻게 확신 할 수 있습니까? 이것이 가능하다면 그것이 실제로 그것을 수행하는지 확인해야합니다. 컴파일러 옵티마이 저가 선택하기를 바라는 것이 아닙니다 (작동 할 수도 있고 작동하지 않을 수도있는 인라인)
Toad

6
@hogan : 나는 정정 당했다. Gcc와 MSVC가 모두 꼬리 재귀 최적화를 수행한다는 것이 맞습니다.
Toad

5
이 예제는 마지막 재귀 호출이 아니기 때문에 꼬리 재귀가 아닙니다.
Brian Young

4

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

내가 본 일반적인 반 패턴은 다음과 같은 선을 따른다.

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의 경우 호출 비용이 너무 많이 들지는 않지만 확실히 비용이 발생합니다 (일반적으로 "객체가 생성되었는지 확인하고 생성되지 않은 경우 생성 한 다음 반환합니다). 이 게터 체인이 더 복잡해질수록 더 많은 시간을 낭비하게됩니다.


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

  2. const가능할 때마다 사용

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

이들 중 처음 2 개, 특히 # 1은 최적화 프로그램이 코드를 분석하는 데 도움이됩니다. 특히 레지스터에 유지할 변수에 대해 좋은 선택을하는 데 도움이됩니다.

맹목적으로 register 키워드를 사용하는 것은 최적화를 해치는 것만 큼 도움이 될 것입니다. 어셈블리 출력이나 프로필을 볼 때까지 무엇이 중요한지 아는 것은 너무 어렵습니다.

코드에서 좋은 성능을 얻는 데 중요한 다른 사항이 있습니다. 예를 들어 캐시 일관성을 최대화하기 위해 데이터 구조를 설계합니다. 그러나 문제는 옵티 마이저에 관한 것이 었습니다.



3

한 번 만났던 일이 떠 올랐는데, 증상은 단순히 메모리가 부족하다는 것이었지만 그 결과 성능이 크게 향상되었습니다 (메모리 사용량이 크게 감소했습니다).

이 경우 문제는 우리가 사용하던 소프트웨어가 많은 양의 작은 할당을했다는 것입니다. 예를 들어, 여기에 4 바이트, 여기에 6 바이트 등을 할당합니다. 많은 작은 객체도 8-12 바이트 범위에서 실행됩니다. 문제는 프로그램이 많은 작은 것들을 필요로하는 것이 아니라 많은 작은 것을 개별적으로 할당했기 때문에 (이 특정 플랫폼에서) 32 바이트로 각 할당이 커졌다는 것입니다.

해결책의 일부는 Alexandrescu 스타일의 작은 개체 풀을 구성하는 것이었지만이를 확장하여 작은 개체의 배열과 개별 항목을 할당 할 수있었습니다. 한 번에 더 많은 항목이 캐시에 들어가기 때문에 성능도 크게 향상되었습니다.

솔루션의 다른 부분은 수동으로 관리되는 char * 멤버의 만연한 사용을 SSO (작은 문자열 최적화) 문자열로 대체하는 것이 었습니다. 최소 할당은 32 바이트이고 char * 뒤에 28 자 버퍼가 포함 된 문자열 클래스를 만들었으므로 문자열의 95 %가 추가 할당을 수행 할 필요가 없었습니다 (그런 다음 거의 모든 모양을 수동으로 교체했습니다. 이 새로운 클래스와 함께이 라이브러리의 char *, 재미 있든 없든). 이로 인해 메모리 조각화도 크게 도움이되었고, 다른 지적 대상 객체에 대한 참조 위치가 증가했으며 마찬가지로 성능이 향상되었습니다.


3

이 답변에 대한 @MSalters 주석에서 배운 깔끔한 기술을 사용하면 컴파일러가 일부 조건에 따라 다른 객체를 반환 할 때도 복사 제거를 수행 할 수 있습니다.

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

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

2

반복적으로 호출하는 작은 함수가있는 경우 이전에는 헤더에 "정적 인라인"으로 입력하여 큰 이득을 얻었습니다. ix86의 함수 호출은 놀랍도록 비쌉니다.

명시 적 스택을 사용하여 비재 귀적 방식으로 재귀 함수를 다시 구현하는 것도 많은 이점을 얻을 수 있지만 실제로는 개발 시간 대 이득의 영역에 있습니다.


재귀를 스택으로 변환하는 것은 광선 추적기를 개발하고 다른 렌더링 알고리즘을 작성하는 사람들을 위해 ompf.org에서 가정 된 최적화입니다.

... 내 개인 레이트 레이서 프로젝트에서 가장 큰 오버 헤드는 합성 패턴을 사용하는 경계 볼륨 계층을 통한 vtable 기반 재귀라는 점을 추가해야합니다. 실제로는 트리로 구조화 된 중첩 된 상자 묶음이지만 패턴을 사용하면 데이터 부풀림 (가상 테이블 포인터)이 발생하고 명령 일관성이 감소합니다 (작고 단단한 루프가 이제는 함수 호출 체인이 됨)
Tom

2

두 번째 최적화 조언이 있습니다. 첫 번째 조언과 마찬가지로 이것은 언어 나 프로세서에 특정한 것이 아니라 일반적인 목적입니다.

컴파일러 매뉴얼을 철저히 읽고 그것이 무엇을 말하는지 이해하십시오. 컴파일러를 최대한 사용하십시오.

나는 프로그램에서 성능을 끌어내는 데 중요한 것으로 올바른 알고리즘을 선택하는 것으로 확인한 다른 응답자 중 한 두 명에 동의합니다. 그 외에도 컴파일러 사용에 투자 한 시간에 대한 수익률 (코드 실행 개선으로 측정)은 코드 조정의 수익률보다 훨씬 높습니다.

예, 컴파일러 작성자는 코딩 거인의 경주 출신이 아니며 컴파일러에는 실수가 포함되어 있으며 매뉴얼과 컴파일러 이론에 따르면 작업 속도를 높이면 작업 속도가 느려집니다. 그렇기 때문에 한 번에 한 단계 씩 수행하고 조정 전후 성능을 측정해야합니다.

그리고 예, 궁극적으로 컴파일러 플래그의 조합 폭발에 직면 할 수 있으므로 다양한 컴파일러 플래그로 make를 실행하고 대규모 클러스터에서 작업을 대기열에 넣고 런타임 통계를 수집하려면 스크립트 한두 개가 필요합니다. PC에서 사용자와 Visual Studio 만 있으면 충분한 컴파일러 플래그 조합을 시도하기 훨씬 전에 관심이 없어집니다.

문안 인사

코드 조각을 처음 선택하면 일반적으로 1.4 ~ 2.0 배 더 많은 성능을 얻을 수 있습니다 (즉, 새 버전의 코드는 이전 버전의 1 / 1.4 또는 1/2 시간에 실행 됨). 컴파일러 플래그를 조작하여 하루 또는 이틀. 당연히, 그것은 내가 작업하는 코드의 대부분을 만들어 낸 과학자들 사이에 컴파일러에 대한 지식이 부족하다는 의견 일 수 있습니다. 컴파일러 플래그를 최대로 설정하면 (드물게 -O3가 아님) 1.05 또는 1.1의 또 다른 요소를 얻으려면 몇 달 간의 노력이 필요할 수 있습니다.


2

DEC가 알파 프로세서와 함께 나왔을 때 컴파일러는 항상 최대 6 개의 인수를 레지스터에 자동으로 입력하려고 시도하므로 함수에 대한 인수 수를 7 미만으로 유지하라는 권장 사항이있었습니다.


x86-64 비트는 또한 많은 레지스터 전달 매개 변수를 허용하므로 함수 호출 오버 헤드에 큰 영향을 미칠 수 있습니다.

1

성능을 위해 먼저 구성 요소 화, 느슨하게 결합 된 등의 유지 관리 가능한 코드 작성에 중점을 두십시오. 따라서 재 작성, 최적화 또는 단순히 프로파일 링을 위해 부품을 분리해야 할 때 많은 노력없이 수행 할 수 있습니다.

Optimizer는 프로그램의 성능에 약간 도움이됩니다.


3
이는 커플 링 "인터페이스"자체가 최적화에 적합한 경우에만 작동합니다. 인터페이스는 본질적으로 "느려질"수 있습니다. 예를 들어 중복 조회 또는 계산을 강제하거나 잘못된 캐시 액세스를 강제 할 수 있습니다.

1

여기에서 좋은 답을 얻고 있지만 그들은 당신의 프로그램이 처음부터 최적에 가깝다고 가정하고

프로그램이 올바로 작성되고, 전체 최적화로 컴파일되고, 테스트되어 프로덕션에 투입되었다고 가정합니다.

내 경험상 프로그램이 올바르게 작성 될 수 있지만 이것이 최적에 가깝다는 의미는 아닙니다. 그 지점에 도달하려면 추가 작업이 필요합니다.

예를 들어 보면 이 답변매크로 최적화를 통해 완벽하게 합리적으로 보이는 프로그램이 어떻게 40 배 이상 빠르게 만들어 졌는지 보여줍니다 . 모든 분야 에서 큰 속도 향상을 할 수는 없습니다.처음 작성된 것처럼 프로그램 많은 경우 (매우 작은 프로그램 제외) 내 경험상 가능합니다.

그 후, 마이크로 최적화 (핫스팟)를 통해 좋은 결과를 얻을 수 있습니다.


1

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

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

코드가 계산 용이고 많은 루프를 포함하는 경우-인텔 컴파일러의 벡터화 보고서가 매우 유용합니다. 도움말에서 'vec-report'를 찾으십시오.

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


당신은 질문에 대한 대답에 가까워지고 있습니다 ..... 컴파일러가 이러한 종류의 최적화를 수행 할 수 있도록 코드에 어떤 종류의 작업을 수행합니까?
EvilTeach

1
C 스타일로 더 많이 작성하려고 시도합니다 (C ++의 경우와 비교). 예를 들어 절대적으로 필요하지 않은 가상 함수를 피하고, 특히 자주 호출 될 경우 AddRefs .. 및 모든 멋진 기능을 피하십시오 (정말 필요하지 않은 경우). 인라이닝하기 쉬운 코드를 작성하십시오. 매개 변수가 적고 "if"-s가 적습니다. 절대적으로 필요하지 않으면 전역 변수를 사용하지 마십시오. 데이터 구조에서-더 넓은 필드를 먼저 배치 (double, int64가 int 앞에 옴)-컴파일러가 첫 번째 필드의 자연 크기에 구조체를 정렬-성능에 적합합니다.
jf.

1
데이터 레이아웃 및 액세스는 성능에 절대적으로 중요합니다. 그래서 프로파일 링 후-나는 때때로 접근 지역에 따라 구조를 여러 구조로 나눕니다. 한 가지 더 일반적인 트릭-int 또는 size-t 대 char 사용-데이터 값이 작더라도 다양한 성능을 피하십시오. 페널티는로드 블로킹에 저장되며, 부분 레지스터 중단 문제. 물론 이러한 데이터의 큰 배열이 필요한 경우에는 적용 할 수 없습니다.
jf.

한 가지 더-실제 필요가없는 한 시스템 호출을 피하십시오 :)-매우 비쌉니다
jf.

2
@jf : 답변을 +1했지만 댓글에서 답변 본문으로 답변을 이동해 주시겠습니까? 읽기가 더 쉬울 것입니다.
kriss

1

내가 C ++에서 사용한 한 가지 최적화는 아무것도하지 않는 생성자를 만드는 것입니다. 개체를 작업 상태로 전환하려면 수동으로 init ()를 호출해야합니다.

이 클래스의 큰 벡터가 필요한 경우 이점이 있습니다.

벡터에 대한 공간을 할당하기 위해 reserve ()를 호출하지만 생성자는 실제로 객체가있는 메모리 페이지를 건드리지 않습니다. 그래서 나는 약간의 주소 공간을 소비했지만 실제로 많은 물리적 메모리를 소비하지는 않았습니다. 나는 관련 건설 비용과 관련된 페이지 오류를 피합니다.

벡터를 채우기 위해 객체를 생성 할 때 init ()를 사용하여 설정했습니다. 이것은 전체 페이지 폴트를 제한하고 벡터를 채우는 동안 크기를 조정할 필요가 없습니다.


6
std :: vector의 일반적인 구현은 더 많은 용량을 예약 () 할 때 실제로 더 많은 객체를 생성하지 않는다고 생각합니다. 페이지를 할당합니다. 생성자는 나중에 배치 new를 사용하여 호출됩니다. 실제로 벡터에 객체를 추가 할 때 (아마도) init ()를 호출하기 직전이므로 별도의 init () 함수가 필요하지 않습니다. 또한 생성자가 소스 코드에서 "비어있는"경우에도 컴파일 된 생성자는 가상 테이블 및 RTTI와 같은 항목을 초기화하는 코드를 포함 할 수 있으므로 어쨌든 생성시 페이지가 수정됩니다.
Wyzard

1
네. 우리의 경우 벡터를 채우기 위해 push_back을 사용합니다. 객체에는 가상 기능이 없으므로 문제가되지 않습니다. 생성자로 처음 시도했을 때 페이지 오류의 양에 놀랐습니다. 나는 무슨 일이 일어 났는지 깨달았고 우리는 생성자의 용기를 잡아 당 겼고 페이지 폴트 문제는 사라졌습니다.
EvilTeach

오히려 놀랍습니다. 어떤 C ++ 및 STL 구현을 사용 했습니까?
David Thornley

3
나는 다른 사람들과 동의합니다. 이것은 std :: vector의 잘못된 구현처럼 들립니다. 객체에 vtable이 있더라도 push_back까지 생성되지 않습니다. 필요한 모든 벡터는 push_back의 복사 생성자이기 때문에 기본 생성자를 비공개로 선언하여이를 테스트 할 수 있어야합니다.

1
@David-구현은 AIX에있었습니다.
EvilTeach 2010-04-07

1

내가 한 한 가지는 사용자가 프로그램이 약간 지연 될 것으로 예상 할 수있는 곳에 값 비싼 작업을 유지하려고하는 것입니다. 전반적인 성능은 응답 성과 관련이 있지만 동일하지는 않으며 많은 경우 응답 성이 성능의 더 중요한 부분입니다.

마지막으로 전체 성능을 개선해야했을 때 차선 알고리즘을 주시하고 캐시 문제가있을 가능성이있는 곳을 찾았습니다. 먼저 성능을 프로파일 링하고 측정 한 다음 각 변경 후에 다시 측정했습니다. 그 후 회사는 무너졌지만 어쨌든 흥미롭고 유익한 작업이었습니다.


0

나는 오랫동안 의심해 왔지만, 배열을 선언하여 요소의 수로 2의 거듭 제곱을 유지하도록하면 옵티마이 저가 조회 할 때 비트 수만큼 시프트로 곱하기를 대체하여 강도 감소 를 수행 할 수 있음을 증명하지 못했습니다. 개별 요소.


6
예전에는 사실 이었지만 이제는 더 이상 그렇습니다. 사실 그 반대는 사실입니다. 2의 거듭 제곱으로 배열을 선언하면 메모리에서 2의 거듭 제곱만큼 떨어져있는 두 포인터에서 작업하는 상황에 처할 가능성이 큽니다. 문제는 CPU 캐시가 그렇게 구성되어 있고 두 개의 어레이가 하나의 캐시 라인 주위에서 싸우게 될 수 있다는 것입니다. 그런 식으로 끔찍한 성능을 얻습니다. 포인터 중 하나를 2 바이트 앞에두면 (예 : 2의 제곱이 아닌) 이러한 상황을 방지 할 수 있습니다.
Nils Pipenbrinck

+1 Nils,이 중 하나는 Intel 하드웨어에서 "64k aliasing"입니다.
Tom

그건 그렇고, 이것은 분해를 보면 쉽게 반증되는 것입니다. 몇 년 전에 gcc가 시프트와 덧셈으로 모든 종류의 상수 곱셈을 최적화하는 방법을보고 놀랐습니다. 예 val * 7를 들어, 그렇지 않으면 어떻게 보일지로 변했습니다 (val << 3) - val.
dash-tom-bang

0

작고 자주 호출되는 함수를 소스 파일의 맨 위에 놓으십시오. 그러면 컴파일러가 인라인 기회를 더 쉽게 찾을 수 있습니다.


정말? 이에 대한 근거와 예를 인용 할 수 있습니까? 사실이 아니라고 말하는 것이 아니라 위치가 중요 할 것입니다.
underscore_d

@underscore_d 함수 정의가 알려질 때까지 인라인 할 수 없습니다. 현대 컴파일러는 코드 생성 시간에 정의가 알려 지도록 여러 번 통과 할 수 있지만 나는 그것을 가정하지 않습니다.
Mark Ransom

나는 컴파일러가 물리적 함수 순서가 아닌 추상 호출 그래프에서 작동한다고 가정했는데, 이는 중요하지 않다는 것을 의미합니다. 물론, 특별히주의를 기울여도 나쁘지 않다고 생각합니다. 특히 성능을 제쳐두고 IMO를 호출하는 함수보다 먼저 호출되는 함수를 정의하는 것이 더 논리적 인 것처럼 보입니다. 성능을 테스트해야하는데 중요하다면 놀랄 것이지만 그때까지는 놀라 울 수 있습니다!
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.