요즘들어서는 컴퓨터의 사양이 높아지면서 저를 포함한 많은 프로그래머(개발자^^;;) 들이
퍼포먼스에 대해서는 별로 신경을 쓰지 않는것 같습니다.
연산을 하는데 드는 비용따위는 신경안써도 프로그램은 잘 돌아가니 뭐 귀찮게 그런거
생각하기도 싫고 그런거 신경쓰다가 오히려 생산성(시간)이 떨어져 납기일 못맞추는것 보다는
그냥 손가는 데로 코딩하는게 속편하다고 생각하죠.
하지만 방심하다가 낭패를 보는경우가 종종 있기도 합니다.
개발툴이 개발자 위주로 편하게 나오다보니 그 무게를 고사양PC도 감당하기 힘들지요.
이런전차로 코드를 튜닝하고자하는 개발자들을 위해 참고가 될만한 자료를 올립니다.
■ 시간단축을 위한 공간규칙
① Data Structure Augmentation
- 빈번한 연산을 하는데 필요한 시간은 종종 부가적 정보로 데이터 구조를 늘리거나
또는 데이터 구조 내의 정보를 변경하여 더 쉽게 접근할 수 있도록 함으로써
감소시킬 수 있다
② Store Precomputed Results
- 비용이 많이 드는 함수값을 다시 계산하는 비용은 함수를 한번만 계산하고
그 결과를 저장하는 방법으로 감소시킬 수 있다. 그 이후에 함수가 호출될 때는
다시 계산하기보다는 테이블을 검색하여 처리한다.
③ Cashing
- 가장 빈번하게 접근하는 데이터는 그 접근 비용이 가장 적어야 한다.
④ Lazy Evaluation
- 어떤 아이템이 실제로 필요하기 전까지는 평가(evalution)를 하지 않음으로써
아이템에 대한 불필요한 평가를 피한다.
■ 공간절약을 위한 시간규칙
① Packing
- 촘촘한 데이터 구조를 사용하면 데이터를 저장하고 검색하는 데 필요한 시간은
늘어나지만 메모리비용을 줄일 수 있다.
- 패킹은 종종 메모리를 절약하기 위해 시간을 희생하는 것이지만,
때로는 더 작은 데이터 표현으로 인해 처리속도가 더 빨라질 수도 있다.
② Interpreters
- 어떤 프로그램을 표현하기 위한 메모리는 많이 발생하는 일련의 오퍼레이션을
간결하게 표현하는 인터프리터를 사용하여 감소시킬 수 있는 경우가 종종 있다.
■ 루프 규칙
① Code Motion Out of Loops
- 어떤 계산을 루프가 반복될 때마다 실행하는 것보다는 루프 밖에서 한번만 실행하는
것이 낫다.
② Combining Tests
- 효율적인 내부 루프(inner loop)는 가장 적은 테스트를 포함하고 있어야하며,
단지 하나의 테스트만을 포함하고 있는 것이 바람직하다. 따라서 프로그래머는
루프의 몇 가지 종료 조건을 다른 종료 조건을 이용하여 시뮬레이트해봐야 한다.
③ Loop Unrolling
- 루프를 펼치는 것은 루프 인덱스 값을 변경해야 하는 비용을 제거하고,
또한 파이프라인 지연(pipeline stall)을 피하도록 도와주며,분기를 감소시키고,
명령어(instruction) 수준의 병렬처리를 증가시킨다.
④ Transfer-Drven Loop Unrolling
- 만약 내부 루프에서 많은 비용이 단순한 대입문에 소모된다면,
이런 대입문은 코드를 반복하고 변수의 사용을 배경하여 제거할 수 있다.
특히, i=j와같은 대입문을 제거하면, 이와 관련된 코드에서 j가 i 인것처럼 다루어야한다.
⑤ Unconditional Branch Removal
- 빠른 루프에는 무조건적 분기를 포함하면 안된다.
루프의 마지막 부분에 있는 무조건적 분기는 루프를 회전시켜 루프의 마지막에 조건적
분기를 갖도록 하여 제거할 수 있다.
⑥ Loop Fusion
- 근처에 있는 두 루프가 같은 요소의 집합에 대해 동작하고 있다면,
동작 부분을 묶어 하나의 루프에서 처리하도록 한다.
■ 논리규칙
① Exploit Algebraic Identities
- 만약 논리식을 평가하는 데 비용이 많이 든다면, 평가하는 데 비용이 덜 드는 대수적으
로 동치인 논리식으로 대체한다.
② Short-Circuiting Monotone Functions
- 만약 여러변수에 대한 어떤 단조 증가 함수가 특정 한계를 초과하는지 검사하려 할 때,
특정 한계를 한 번 넘은 다음에는 다른 변수에대해서 평가를 할 필요가 없다.
③ Reordering Tests
- 논리식은비용이 적게 들고 결과가 참인 경우가 많은 식이 비용이 많이 들고 결과가
참인 경우가 거의 없는 식보다 앞에 와야 한다.
④ Precompute Logical Functions
- 작고 유한한 도메인에 대한 논리 함수는 그 도메인을 나타내는 테이블에 대한 검색으로
대체할 수 있다.
⑤ Boolean Variable Elimination
- 불리언(Boolean)변수 v에대한 대입식을 v가 참일 때와 거짓일 대를 처리하는
if-else문으로 바꾸어 프로그램에서 불리언 변수를 제거할 수 있다.
■ 프로시저 규칙
① Collasping Function Hierarchies
- 함수가 다른 함수를 호출하는 구조로 되어 있는 경우 호출되는 함수를 인라인화하고
넘겨지는 변수를 묶어서 함수를 재작성하면 종종 실행시간을 단축할 수 있다.
② Exploit Common Cases
- 함수는 모든 경우를 정확히 처리하고 빈번한 경우에 대해서는 효율적으로
처리해야 한다.
③ Coroutines
- 다중 패스(multi-pass) 알고리즘은 종종 협동루틴(coroutines)을 사용하여
단일 패스(single-pass) 알고리즘으로 바꿀 수 있다.
④ Transformations on Recursive Functions
- 명시적인 프로그램 스택을 사용하여 재귀를 반복으로 변환한다.
- 만약 함수의 마지막 부분에서 자신을 재귀적으로 호출하면, 그부분을 함수의
첫 번째 부분으로 분기하도록 바꿀 수 있는데, 이것을 Removing Tail Recursion이라고
도 한다.
- 작은 부분문제를 풀 때는 재귀를 반복하여 크기가 0 또는 1 에대한 문제를 풀게 하는 것
보다는 보조 프로시저를 사용하는 편이 더 효율적일 수 있다.
⑤ Paralelism
- 하드웨어가 병렬처리를 지원하는 경우, 프로그램은 가능한 많이 병렬처리를 사용할 수
있는 구조로 작성되어야 한다.
■ 수식 규칙
① Compile-Time Initialization
- 가능한 많은 변수가 프로그램이 실행되기 전에 초기화되어 있어야 한다.
② Exploit Algebraic Identities
- 만약 평가하는 비용이 많이 드는 수식이 있다면, 평가하는 데 비용이 적게 들면서
대수적으로 동치인 수식으로 바꾼다.
- 배열의 모든 요소에 대해 반복하는 루프에서 곱셈을 계산하는 데 덧셈을 이용하도록
하면 연산 강도를 줄일 수 있다. 많은 컴파일러가 이 최적화를 수행한다.
이 기법은 점증적 알고리즘의 여러 경우로 일반화 될 수 있다.
③ Common Subexpression Elimination
- 만약 포함된 변수가 변하지 않은 채로 동일한 수식이 두 번 평가된다면, 첫번째 평가
결과를 저장한 다음 두 번째로 수식을 평가하는 부분에서 사용하여 같은 수식을 두번
평가하는 것을 피할 수 있다.
④ Pairing Computation
- 만약 비슷한 두 수식이 빈번하게 함께 평가된다면, 그 둘을 하나의 쌍으로 평가하는
새로운 프로시저를 만들어야 한다.
⑤ Exploit Word Parallelism
- 비용이 많이 드는 수식을 평가할 때는 컴퓨터 하부 구조가 제공하는 기본적인 데이터
경로의 폭을 최대로 사용하라.
※출처 : 존 벤틀리 의 생각하는 프로그래머 중에서....
역자주)코드 튜닝은 코드의 가독성을 떨어뜨려 코드를 이해하기 어렵게 만드는 것이 보통이다. 따라서 코드 튜닝은 프로그램의 퍼포먼스가 정말로 문제가 될 때까지 미루는 것이 좋을 것이다.
이상입니다.
참고하세요.
[출처] 코드튜닝을 위한 지침|작성자 하드코더
덧글|신고