컴파일러의 For문 최적화
어떠한 조건에서 컴파일러는 For문을 최적화 하는가?
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
CPU 입장에서 For문에 사용되는 JMP 명령어는, 파이프라인을 깨뜨리거나, 분기 예측 실패시, 성능 저하가 일어나기 때문에, 컴파일러는 조건만 맞으면 JMP 명령어를 사용하지 않으려고 한다.
- 반복 횟수가 컴파일 타임에 상수로 확정될때.
최적화 가능: for (int i = 0; i < 4; i++) (4번 반복됨을 앎) 최적화 어려움: for (int i = 0; i < n; i++) (n이 런타임에 결정되는 변수일 경우)
// 원본 코드
for (int i = 0; i < 3; i++) {
arr[i] = 0;
}
// 컴파일러 최적화 후
arr[0] = 0;
arr[1] = 0;
arr[2] = 0;
최적화 전 어셈블리어
.L2:
cmp DWORD PTR [rbp-4], 2 ; i < 3 인지 비교
jg .L3 ; 조건이 안 맞으면 루프 종료(L3로 점프)
mov eax, DWORD PTR [rbp-4]
cdqe
mov DWORD PTR [rbp-32+rax*4], 0 ; arr[i] = 0 대입
add DWORD PTR [rbp-4], 1 ; i++
jmp .L2 ; 다시 루프 시작점(.L2)으로 무조건 점프 (JMP)
.L3:
최적화 된 어셈블리어
mov DWORD PTR [rdi], 0 ; arr[0] = 0
mov DWORD PTR [rdi+4], 0 ; arr[1] = 0 (int는 4바이트이므로 +4)
mov DWORD PTR [rdi+8], 0 ; arr[2] = 0 (int는 4바이트이므로 +8)
- 컴파일 타임에 최종 결과값을 미리 계산 할 수 있을때.
루프 내부의 로직이 외부 입출에 의존 하지 않을때, 결과값을 상수로 치환해버린다.
// 원본 코드
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
// 컴파일러 최적화 후
// JMP는 커녕 덧셈 연산조차 런타임에 하지 않는다.
return 5050;
최적화 전 어셈블리어
.L5:
cmp DWORD PTR [rbp-4], 100 ; i <= 100 인지 비교
jg .L6 ; 아니면 루프 종료
mov eax, DWORD PTR [rbp-4]
add DWORD PTR [rbp-8], eax ; sum += i
add DWORD PTR [rbp-4], 1 ; i++
jmp .L5 ; 다시 루프 시작점(.L5)으로 점프 (JMP)
.L6:
mov eax, DWORD PTR [rbp-8] ; 결과값 반환 준비
최적화 후 어셈블리어
mov eax, 5050 ; 계산된 결과값 5050을 eax 레지스터에 삽입
ret ; 즉시 반환
- 루프 전개를 해도 코드 길이가 짧을때.
1번 코드 처럼 루프를 전개하는것을 루프 전개 또는 Loop Unrolling이라 한다. 루프 전개를 함으로써 코드 길이가 너무 길어진다면, CPU의 명령어 캐시 메모리를 낭비하는 셈이 되므로 루프 내부의 코드 크기가 작지않다면, Loop Unrolling 최적화를 하지 않는다.
- 루프 내부에 복잡한 제어 흐름이 없을때.
루프 내부에 break, continue, goto, 또는 복잡한 if-else문이 있거나, 외부 함수 호출이 포함되어 있으면 컴파일러는 함부로 루프를 전개하지 않는다.
printf 같은 I/O 함수가 들어있다면, 화면에 출력하는 부수 효과를 지켜야 하므로 루프를 생략하지 않고 정직하게 JMP를 돌며 함수를 호출한다.
번외로 컴파일러 최적화 옵션이 켜져있어야 위처럼 최적화를 시도한다.