실행 환경
- ARMv8 아키텍처 & IAR 컴파일러
- Ofast 최적화 적용
Info
해당 글에서는 for 루프가 어셈블리로 어떻게 처리되는지 확인해본다.
개요
for(int i = 0; i < 10; i++)
{
ret[i] += i;
}for 반복문에는 조건을 검사하는 과정이 포함되어 있으며, 이 과정이 선형적으로 수행되는 형태라면 이를 다음과 같이 풀어 쓸 수 있다.
if (i == 0) ret[i] += i;
else if (i == 1) ret[i] += i;
else if (i == 2) ret[i] += i;
// … 이하 생략 …물론 어셈블리 코드는 이와 형태가 많이 다르다.
하지만 이는 비효율적인 방식이다. 조건을 검사해야 할 뿐만 아니라, 그 조건에 사용되는 값에 대한 연산도 함께 수행해야 하기 때문이다.
이러한 이유로, 최적화 기법 중에는 루프를 사용하지 않고 연산을 직접 펼치는 loop unrolling 방식이 존재한다.
실제로 for 반복문의 반복횟수가 적을 경우, 컴파일러 최적화 과정에서 loop unrolling이 적용되기도 한다.
이에 대해서 더 알아보자.
반복 횟수가 18개 보다 적을 경우
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
volatile int ret = choice;
for (int i = 0; i < 17; i++) {
ret += i;
}
return ret;
}위 반복문을 컴파일하면 아래와 같은 방식으로 어셈블리어가 생성된다.
test:
341808b0: sub sp, #8
223 volatile int ret = choice;
341808b2: str r0, [sp, #4]
226 ret += i;
341808b4: ldr r3, [sp, #4]
341808b6: str r3, [sp, #4]
341808b8: ldr r3, [sp, #4]
341808ba: adds r3, #1
341808bc: str r3, [sp, #4]
341808be: ldr r3, [sp, #4]
341808c0: adds r3, #2
341808c2: str r3, [sp, #4]
341808c4: ldr r3, [sp, #4]
341808c6: adds r3, #3
341808c8: str r3, [sp, #4]
341808ca: ldr r3, [sp, #4]
341808cc: adds r3, #4
341808ce: str r3, [sp, #4]
341808d0: ldr r3, [sp, #4]
341808d2: adds r3, #5
341808d4: str r3, [sp, #4]
341808d6: ldr r3, [sp, #4]
341808d8: adds r3, #6
341808da: str r3, [sp, #4]
341808dc: ldr r3, [sp, #4]
341808de: adds r3, #7
341808e0: str r3, [sp, #4]
341808e2: ldr r3, [sp, #4]
341808e4: adds r3, #8
341808e6: str r3, [sp, #4]
341808e8: ldr r3, [sp, #4]
341808ea: adds r3, #9
341808ec: str r3, [sp, #4]
341808ee: ldr r3, [sp, #4]
341808f0: adds r3, #10
341808f2: str r3, [sp, #4]
341808f4: ldr r3, [sp, #4]
341808f6: adds r3, #11
341808f8: str r3, [sp, #4]
341808fa: ldr r3, [sp, #4]
341808fc: adds r3, #12
341808fe: str r3, [sp, #4]
34180900: ldr r3, [sp, #4]
34180902: adds r3, #13
34180904: str r3, [sp, #4]
34180906: ldr r3, [sp, #4]
34180908: adds r3, #14
3418090a: str r3, [sp, #4]
3418090c: ldr r3, [sp, #4]
3418090e: adds r3, #15
34180910: str r3, [sp, #4]
34180912: ldr r3, [sp, #4]
34180914: adds r3, #16
34180916: str r3, [sp, #4]
228 return ret;
34180918: ldr r0, [sp, #4]
229 }
3418091a: add sp, #8
3418091c: bx lr
3418091e: nop
238 volatile int a = 2;
18개 보다 많을 경우
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
volatile int ret = choice;
for (int i = 0; i < 18; i++) {
ret += i;
}
return ret;
} test:
341808b0: push {lr}
225 for (int i = 0; i < 18; i++) {
341808b2: mov.w lr, #18
341808b6: movs r2, #0
341808b8: dls lr, lr
222 int test(int choice) {
341808bc: sub sp, #12
223 volatile int ret = choice;
341808be: str r0, [sp, #4]
226 ret += i;
341808c0: ldr r3, [sp, #4]
341808c2: add r3, r2
341808c4: str r3, [sp, #4]
225 for (int i = 0; i < 18; i++) {
341808c6: adds r2, #1
341808c8: le lr, 0x341808c0 <test+16>
228 return ret;
341808cc: ldr r0, [sp, #4]
229 }
341808ce: add sp, #12
341808d0: ldr.w pc, [sp], #4
238 volatile int a = 2;결론
- 18개 보다 비교가 적다면 코드가 분기 없이 펼쳐져서 동작
- 18보다 비교가 많다면 분기로 처리
이는 컴파일러나 옵션에 따라서 다른 결과를 가질 것이다. 따라서 의도적으로 loop unrolling을 구현하지 않아도 적절히 최적화될 수도 있다는 것만 참고하도록 하자.
정말 정확한 동작을 확인하고 싶다면, 어셈블리 코드를 직접 확인하는 것이 좋다.