1. 선언 배치
1) 할당 보다 초기화
2) 선언 재배치
- 쓸 떼 없이 객체서 생성 및 소멸 되지 않도록 배치
3) 멤버 초기화 리스트
4) Prefix, Postfix 연산자
- --과 ++는 뒤에 붙이는 것(postfix)보다 앞에 붙여서 사용(prefix)하는 것이 좋음
2. 인라인 함수
1) 함수 호출 오버헤드는 구현에 따라 다름
- 스택 위치를 저장하고 스택으로 함수의 인수를 넣고 초기화하고 함수 정의를 포함하는 메모리 주소 점프함 => 그 이후 함수가 실행
- 함수 리턴시 역순
- 함수 호출 이전과 이후에 수행하는 추가 동작으로 함수 호출 오버헤드가 두드러지는 언어도 존재
- 인라인 함수는 멤버함수 중 accessor(getter)와 mutator(setter)의 효율적인 구현을 허용하기 위해 추가됨
- 비 멤버 함수 또한 인라인으로 될 수 있음
2) 이점
- 인수와 반환값을 가질 수 있고 자신만의 scope(범위)를 갖는다.
- 매크로 보다 더 안전하고 디버깅하기 쉽다.
- 함수 코드 단독으로 수행 할 수 없는 문맥에 특화된 최적화를 적용할 수 있다.
- 클래스 안에 구현되는 모든 멤버 함수들은 암묵적으로 inline으로 선언
- 컴파일러가 자동 생성하는 디폴트 멤버 함수들은 암묵적으로 inline
3. 메모리 최적화
때때로 상호 배타 적이라 메모리 점유를 적게 만드는 것이 더 느린 코드를 초래를 할 수 있고
더 빠른 코드는 더 많은 메모리를 점유 할 수 있다.
1) Bit Fields
- enum은 한 바이트 이하로 안전하게 표현 될 수 있지만 때때로 4바이트 씩 차지할 수 있다.
- Bit Fields를 사용하면 enum 값을 압착 시킬 수 있다.
- 워드 경계로 Padding Byte를 넣을 수 있다 -> 낭비되는 바이트의 댓가로 더 빠른 엑세스 타임을 보장한다.
- 저장 매체에서 보다는 원거리 데이터 전송 및 Web에서 쓸모 있음
ex)
enum CallType
{
toll_free,
local,
regional,
long_distance,
international,
cellular
};
unsigned call : 3;
2) Unions
- 데이터 멤버들 중 적어도 하나의 값이 언제든지 활성화 되는 동일한 메모리 주소에 두 개 이상의 데이터 멤버들을 위치 시킴으로서 메모리 낭비를 최소화 할 수 있다.
- 포인터와 ID를 같이 놓아서 사용할 수 있다.
- 실행 시간 오버헤드를 초래하지 않는다
ex)
union { long n; void * p;}
union
{
char * name;
long ID;
};
4. 속도 최적화
시간이 중요한 어플리케이션에서 CPU 사이클은 중요함
1) 긴 인수 리스트를 압축하기 위하여 클래스를 사용
- 함수 호출의 오버헤드는 함수가 인수들의 긴 리스트를 가질 때 증가 된다.
- 런타임 시스템은 스택에 인수의 값을 초기화 해야 하므로 더 많은 인수가 있을 때 시간이 더 걸린다.
- 인수의 리스트를 단일 클래스로 압축하고 참조로 인수를 전달하면 속도를 단축 시킬 수 있다.
- 실행하면서 오랜 시간이 걸리는 함수에 대해서 스택 초기화 오버헤드는 무시할 수준이지만 매우 빈번하게 호출되는 짧고 빠른 함수는 긴 인수 리스트를 단일 객체로 압축하고 참조로 받는 것은 성능을 향상 시킬 수 있다.
2) 레지스터 변수
- register 키워드는 객체가 프로그램에서 다량으로 사용될 것이라고 컴파일러에게 힌트를 주기 위해 사용 될 수 있다.
int* p = new int[3000000];
register int* p2 = p; // store the address in a register
for(register int j = 0; j < 3000000; j++)
{
*p2++ = 0;
}
delete [] p;
- 레지스터에 저장돼 있지 않을 경우 루프의 실행 시간의 상당한 양이 메모리로부터 변수를 페치하고, 새로운 값으로 할당하고 메모리로 다시 저장하는 일을 반복적으로 수행하면서 낭비된다.
- 레지스터에 저장하는 것은 오버헤드를 줄이지만 컴파일러에 대한 권고사항이기 때문에 함수 인라인 화처럼 컴파일러는 객체를 레지스터에 저장하지 않을 수 있다.
- 요즘 컴파일러는 알아서 루프 카운터를 최적화 하고 그것을 어떤 식으로든 레지스터로 옮긴다.
- 레지스터 저장장소 명세는 기본형들로 제한되지 않으며 어떤 종류의 객체로도 사용할 수 있다.
- 객체가 너무 커서 레지스터에 적합하지 않다면 컴파일러는 캐시 메모리와 같은 더 빠른 메모리 영역에 객체를 저장할 수 있다.(캐시 메모리는 메인 메모리보다 약 10배 정도 더 빠르다)
3) 상수 객체들을 const로서 선언
- const로 선언한 상수 객체 또한 컴파일러가 최적화 하여서 메모리 대신에 레지스터에 저장한다.
- 함수 인자 중 const 인자 또한 동일한 최적화가 적용된다.
- 반면 volatile은 그러한 최적화를 막기 때문에 불가피 할 때만 사용해야 한다.
4) 가상 함수들의 실행시간 오버헤드
- 가상 함수가 객체의 포인터나 참조를 통해 호출될 때 호출은 반드시 추가적인 실행시간 패널티를 부과하지 않는다.
- 컴파일러가 호출을 정적으로 분해 할 수 있다면 어떤 부가적인 오버헤드도 초래되지 않는다.
- 매우 짧은 가상 함수는 이러한경우에 인라인화 될 수 있다.
class CParent
{
public:
virtual void show() const { cout << "I'm Parent" << endl; }
};
class CChild : CParent
{
public:
void show() const { cout << "I'm Child" << endl; }
}
void func(CParent& Parent, CParent* pParent)
{
Parent.show();
pParent->show();
}
void func2()
{
CParent Parent;
func(Parent, &Parent);
}
int main()
{
func2();
return 0;
}
- 만일 전체 프로그램이 단일 번역단위속에 나타난다면 컴파일러는 main()에서 함수 func2()의 호출의 인라인 치환을 수행 할 수 있다.
- func2() 내부에서 func()의 invocation 또한 인라인화 될 수 있으며 그래서 func()로 전달되는 인수들의 동적 타입들이 컴파일 시간에 알려지기 때문에 컴파일러는 func()내부에서 가상 함수 호출을 정적으로 분해 할 수 있다.
- 모든 컴파일러가 함수 호출을 인라인화 한다는 보장은 없지만 어떤 컴파일러는 확실히 함수의 인수의 동적 타입이 컴파일 시간에 결정 될 수 있어서 동적 바인딩의 오버헤드를 피할 수 있다는 이점을 취한다.
5) 함수 객체 VS 함수 포인터
5. 최후의 수단
1) RTTI 및 예외처리 지원을 불능으로 만들기
- 순수 C 코드를 C++ 컴파일러로 포팅할 때, 약간의 성능 저하를 경험 할 것이다.
- RTTI 나 예외처리를 지원하기 위해서 C++ 컴파일러는 원래의 C 소스 파일에 추가적인 scaffolding 코드를 삽입하는데 이것은 실행 파일 크기를 약간 증가 시키고 실행시간 오버헤드를 부과한다.
- 순수 C가 사용될 때 이러한 추가적인 코드는 불필요하다.
- new 연산자와 가상 함수들과 같은 어떤 구성요소들을 사용하는 C++나 C에서 쓰면 안된다.
2) 인라인 어셈블리
- C++ 코드에서 시간이 중요한 부분은 원시 어셈블리 코드로 재작성 될 수 있고 결과는 속도가 현저히 빨라질 수 있다.
- 하지만 이 후에 수정을 훨씬 더 어렵게 만들 수 있기 때문에 경솔하게 사용해서는 안된다.
- 코드를 유지보수하는 프로그래머들이 사용된 어셈블리 언어에 익숙하지 않을 수 있거나 사전경험 조차 없을 수 있다.
- 소프트웨어를 다른 플랫폼으로 포팅하는 것은 어셈블리 코드 부분을 재작성하게 할 것이다.
- 어셈블리 코드를 개발하고 테스트하는 것은 고수준 언어로 작성된 코드를 개발하고 테스트하는 것보다 훨씬 더 많은 시간을 소요 할 수 있는 힘든 작업이다.
- 일반 적으로 어셈블리로 코드화된 연산들은 저수준 라이브러리 함수다. memset(), strcpy()
asm
{
mov a, ecx;
// ...
}
3) 운영체제와 직접적으로 상호작용하기
- API 함수들과 클래스는 운영체제와 상호작용할 수 있도록 하지만 때때로 시스템 명령어를 직접 실행하는 것이 훨씬 더 빨라질 수 있다.
- 쉘 명령을 const char*로 취하는 표준함수 system()을 사용할 수 있다.
#include <cstdlib>
int main()
{
system("dir"); // excute the "dir" command
}
- 속도와 호환성 및 미래의 확장성 사이에서 절충 돼야 한다.
6. 결론
이상적인 세계에서, 소프트웨어 설계자들과 개발자들은 그들의 노력을 강인하고 확장가능하고 가독성있는 코드에 초점을 맞출 것이다.
다행히 소프트웨어 세계에서 업무의 현상태는 15, 30 혹은 50년 전보다 그 이상에 훨씬 더 가깝니다.
그럼에도 불구하고 성능조율 및 최적화들은 아마도 오랫동안 필수불가결한 것으로 남을 것이다.
하드웨어가 더 빨라지면 그것 위에서 동작하는 더 많은 소프트웨어가 더 많은 요구들에 부합하도록 요구 된다.
음성 인식, 자연어의 온라인 번역, 신경망, 그리고 복잡한 수학적 계산들은 미래에 발전하고 주의깊은 최적화들을 요구할, 리소스를 포식하는 애플리케이션들의 몇 가지 예들이다.
텍스트 북은 종종 당신이 테스트의 최종 단계로 최적화에 관한 고려를 연기하도록 권한다.
정말로 중요한 목표는 시스템이 정확하게 동작하도록 만드는 것이다.
그럼에도 불구하고 객체들을 지역적으로 선언하는 것, postfix 연산자들 대신 prefix를 선호하는 것 그리고 할당 대신 초기화를 사용하는 것과 같이 여기에서 제시되는 기법들 중 몇 가지는 자연스러운 습관이 될 필요가 있다.
프로그램이 보통 그들 코드의 겨우 10%를 실행하는 데 그들 시간의 90%를 소비한다는 것은 잘 알려진 사실이다.
(수치들은 변하지만 그들은 80%와 20%에서부터 95%와 5%까지의 범위에 있다.)
따라서 최적화의 첫번째 단계는 당신 프로그램들의 그 10%를 판별하여 그것들을 최적화 하는 것이다.
많은 자동화된 프로파일링 및 최적화 툴들은 당신에게 이들 중대한 코드 부분들을 식별하는데 도움을 줄 수 있다.
이들 툴들 중 몇몇은 또한 성능을 향상시키기 위한 솔루션들을 제안 할 수 있다.
여전히 많은 최적화 기법들이 구현에 특정한 것들이며 항상 인간의 전문적인 지식을 요구한다.
아직도 그것들이 정말로 시스템의 성능을 향상시킬 수 있음을 보증하기 위해 당신의 의심을 경험적으로 검증하고 제안된 코드 수정들의 효과를 테스트 하는 것이 중요하다.
어떤 연산들의 비용에 관계되는 프로그래머의 직관은 종종 오해하기 쉽다.
예를 들어, 더 짧은 코드는 더 빠른 코드가 아니다.
비슷하게, 간단한 if문의 비용을 피하기 위해서 뒤얽힌 코드를 작성하는 것은 그것이 오직 하나 또는 두개의 CPU 사이클들만을 절약하기 때문에 고생할 가치가 없다.
'programing > C++' 카테고리의 다른 글
std::tuple (0) | 2016.10.12 |
---|---|
extern과 static (0) | 2016.10.09 |
조이패드(Joypad) Input (0) | 2016.09.09 |
[Modern C++] 스마트 포인터 (0) | 2016.08.16 |
[Modern C++] std::array (0) | 2016.08.16 |