티스토리 뷰
들어가며
C 코드를 작성할 때, "함수의 탈출 조건을 먼저 처리하는 것"과 "변수 선언을 먼저 하는 것"의 성능 차이에 대해 궁금했다.
탈출조건에 먼저 걸리는 클라이언트 코드가 많다면 탈출조건이 먼저 있는 게 좋을 것이고, 그렇지 않은 경우(탈출 조건에 먼저 걸리지 않는 클라이언트 코드가 많은 경우)라도 변수 선언과 탈출조건 중 어떤 것이 상단에 작성되더라도 둘 다 실행되어야 하기 때문에 탈출조건이 상단으로 가는 게 좋다고 생각된다.
분석 지점을 쉽게 표현하자면, 아래 두 포인트로 추려낼 수 있겠다.
- 변수가 선언 시점에 명령어로 접수되는지
- 아니면 사용 시점에 명령어로 접수되는지
- 또한, 선언 뿐만 아니라 초기화까지 했더라도 실제로 사용하는 시점에 명령어로 접수되는지도 궁금해졌다.
만약, 컴파일러 최적화 중 변수가 사용될때 바로 직전 줄에서 할당되도록 한다면 세가지 경우는 같은 어셈블리 코드가 작성될 것이며 성능이 같다고 판단 할 수 있겠다.
추측은 이쯤에서 마무리하고, 실제로 생성되는 어셈블리 코드를 확인해보자.
개발 환경
- Apple M1 Pro
- MacOS Sonoma 14.3
- C89
- clang
- clang -std=c89 -Wall -pedantic-errors
C 언어로 작성하기 (선언)
배열을 받아, 가장 작은 수의 인덱스를 반환하는 코드이다.
버전 1: 탈출 조건 먼저 처리
get_min_index_v1.c
#include <stddef.h>
int get_min_index_v1(const int numbers[], const size_t element_count) {
if (element_count == 0) {
return -1;
}
{
size_t index;
int min_index = 0;
for (index = 1; index < element_count; index++)
{
if (numbers[min_index] > numbers[index])
{
min_index = index;
}
}
return min_index;
}
}
버전 2: 변수 선언 먼저
get_min_index_v2.c
#include <stddef.h>
int get_min_index_v2(const int numbers[], const size_t element_count) {
size_t index;
int min_index;
if (element_count == 0) {
return -1;
}
min_index = 0;
for (index = 1; index < element_count; index++) {
if (numbers[min_index] > numbers[index]) {
min_index = index;
}
}
return min_index;
}
현재까지의 디렉토리 구조는 아래와 같다.
tree -L 1
.
├── get_min_index_v1.c
└── get_min_index_v2.c
어셈블리 코드 생성
clang -std=c89 -Wall -pedantic-errors -S -o get_min_index_v1.s get_min_index_v1.c
clang -std=c89 -Wall -pedantic-errors -S -o get_min_index_v2.s get_min_index_v2.c
어셈블리 코드 생성 후 디렉토리 구조는 아래와 같다.
tree -L 1
.
├── get_min_index_v1.c
├── get_min_index_v1.s
├── get_min_index_v2.c
└── get_min_index_v2.s
버전 1: 탈출 조건 먼저 처리
get_min_index_v1.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 14, 0
.globl _get_min_index_v1 ; -- Begin function get_min_index_v1
.p2align 2
_get_min_index_v1: ; @get_min_index_v1
.cfi_startproc
; %bb.0:
sub sp, sp, #48
.cfi_def_cfa_offset 48
str x0, [sp, #32]
str x1, [sp, #24]
ldr x8, [sp, #24]
subs x8, x8, #0
cset w8, ne
tbnz w8, #0, LBB0_2
b LBB0_1
LBB0_1:
mov w8, #-1
str w8, [sp, #44]
b LBB0_9
LBB0_2:
str wzr, [sp, #12]
mov x8, #1
str x8, [sp, #16]
b LBB0_3
LBB0_3: ; =>This Inner Loop Header: Depth=1
ldr x8, [sp, #16]
ldr x9, [sp, #24]
subs x8, x8, x9
cset w8, hs
tbnz w8, #0, LBB0_8
b LBB0_4
LBB0_4: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #32]
ldrsw x9, [sp, #12]
ldr w8, [x8, x9, lsl #2]
ldr x9, [sp, #32]
ldr x10, [sp, #16]
ldr w9, [x9, x10, lsl #2]
subs w8, w8, w9
cset w8, le
tbnz w8, #0, LBB0_6
b LBB0_5
LBB0_5: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #16]
; kill: def $w8 killed $w8 killed $x8
str w8, [sp, #12]
b LBB0_6
LBB0_6: ; in Loop: Header=BB0_3 Depth=1
b LBB0_7
LBB0_7: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #16]
add x8, x8, #1
str x8, [sp, #16]
b LBB0_3
LBB0_8:
ldr w8, [sp, #12]
str w8, [sp, #44]
b LBB0_9
LBB0_9:
ldr w0, [sp, #44]
add sp, sp, #48
ret
.cfi_endproc
; -- End function
.subsections_via_symbols
버전 2: 변수 선언 먼저
get_min_index_v2.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 14, 0
.globl _get_min_index_v2 ; -- Begin function get_min_index_v2
.p2align 2
_get_min_index_v2: ; @get_min_index_v2
.cfi_startproc
; %bb.0:
sub sp, sp, #48
.cfi_def_cfa_offset 48
str x0, [sp, #32]
str x1, [sp, #24]
ldr x8, [sp, #24]
subs x8, x8, #0
cset w8, ne
tbnz w8, #0, LBB0_2
b LBB0_1
LBB0_1:
mov w8, #-1
str w8, [sp, #44]
b LBB0_9
LBB0_2:
str wzr, [sp, #12]
mov x8, #1
str x8, [sp, #16]
b LBB0_3
LBB0_3: ; =>This Inner Loop Header: Depth=1
ldr x8, [sp, #16]
ldr x9, [sp, #24]
subs x8, x8, x9
cset w8, hs
tbnz w8, #0, LBB0_8
b LBB0_4
LBB0_4: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #32]
ldrsw x9, [sp, #12]
ldr w8, [x8, x9, lsl #2]
ldr x9, [sp, #32]
ldr x10, [sp, #16]
ldr w9, [x9, x10, lsl #2]
subs w8, w8, w9
cset w8, le
tbnz w8, #0, LBB0_6
b LBB0_5
LBB0_5: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #16]
; kill: def $w8 killed $w8 killed $x8
str w8, [sp, #12]
b LBB0_6
LBB0_6: ; in Loop: Header=BB0_3 Depth=1
b LBB0_7
LBB0_7: ; in Loop: Header=BB0_3 Depth=1
ldr x8, [sp, #16]
add x8, x8, #1
str x8, [sp, #16]
b LBB0_3
LBB0_8:
ldr w8, [sp, #12]
str w8, [sp, #44]
b LBB0_9
LBB0_9:
ldr w0, [sp, #44]
add sp, sp, #48
ret
.cfi_endproc
; -- End function
.subsections_via_symbols
어셈블리 코드 분석
내용이 동일하다.
함수의 구조(프롤로그, 에필로그 및 ) 그리고 공통적인 로직 (스택 프레임을 설정하고, 함수 매개변수와 지역 변수를 스택에 저장한 후, 루프를 통해 최솟값을 찾는다. 종료 조건을 만나면 결과를 반환하고 스택을 정리한다.)을 처리하는 코드 외에 분석 대상인 부분을 확인해 보자
분석 대상 짚고 가기
탈출 조건
ldr x8, [sp, #24] ; element_count 로드
subs x8, x8, #0 ; element_count == 0인지 비교
cset w8, ne ; 조건 코드 설정 (element_count != 0인 경우 w8 = 1, 아니면 0)
tbnz w8, #0, LBB0_2 ; w8가 0이 아니면 LBB0_2로 분기 (즉, element_count != 0이면)
b LBB0_1 ; 아니면 LBB0_1로 분기
element_count가 0인지 확인하고, 만약 element_count가 0이면 -1을 반환하도록 하고 함수를 종료한다.
변수 선언
어셈블리 코드에서의 변수 선언은 명시적으로 나타나지 않는다.
대신, 스택 프레임을 설정하면서 필요한 공간을 확보한다.
sub sp, sp, #48 // 스택 포인터를 조정하여 함수의 로컬 변수들을 저장할 공간을 확보
이 명령어는 스택 포인터를 조정하여 함수의 로컬 변수들을 저장할 공간을 확보하는 것이다.
변수 초기화
str wzr, [sp, #12] ; min_index를 0으로 초기화 (wzr는 0 레지스터)
mov x8, #1 ; index를 1로 설정
str x8, [sp, #16] ; index를 스택에 저장
분석
버전 1, 2 (7번째 줄~ 25번째 줄)
; %bb.0:
sub sp, sp, #48 // 스택 포인터를 조정하여 함수의 로컬 변수들을 저장할 공간을 확보
.cfi_def_cfa_offset 48
str x0, [sp, #32]
str x1, [sp, #24]
ldr x8, [sp, #24]
subs x8, x8, #0
cset w8, ne
tbnz w8, #0, LBB0_2
b LBB0_1
LBB0_1:
mov w8, #-1
str w8, [sp, #44]
b LBB0_9
LBB0_2:
str wzr, [sp, #12] // min_index 초기화
mov x8, #1 // index 초기화
str x8, [sp, #16]
b LBB0_3
- LBB0_2 라벨 이후에 min_index와 index가 초기화된다
즉, 탈출 조건 이후에 변수가 초기화된다.
추가적 실험 (선언 & 초기화)
만약 선언만 한 것이 아닌, 초기화까지 한다면 어떻게 될까?
get_min_index_v2_1.c
#include <stddef.h>
int get_min_index_v2(const int numbers[], const size_t element_count) {
size_t index;
- int min_index;
+ int min_index = 0;
if (element_count == 0) {
return -1;
}
- min_index = 0;
for (index = 1; index < element_count; index++) {
if (numbers[min_index] > numbers[index]) {
min_index = index;
}
}
return min_index;
}
min_index 변수를 선언만 한 것이 아니라, 초기화까지 하고 있다.
아래의 명령어로 어셈블리 코드를 생성했다.
clang -std=c89 -Wall -pedantic-errors -S -o get_min_index_v2_1.s get_min_index_v2_1.c
현재까지의 디렉토리 구조는 아래와 같다.
tree -L 1
.
├── get_min_index_v1.c
├── get_min_index_v1.s
├── get_min_index_v2.c
├── get_min_index_v2.s
├── get_min_index_v2_1.c
└── get_min_index_v2_1.s
어셈블리 코드 분석
버전 2_1 (7번째 줄~ 25번째 줄)
; %bb.0:
sub sp, sp, #48 // 스택 포인터를 조정하여 함수의 로컬 변수들을 저장할 공간을 확보
.cfi_def_cfa_offset 48
str x0, [sp, #32]
str x1, [sp, #24]
str wzr, [sp, #12] // min_index 초기화
ldr x8, [sp, #24]
subs x8, x8, #0
cset w8, ne
tbnz w8, #0, LBB0_2
b LBB0_1
LBB0_1:
mov w8, #-1
str w8, [sp, #44]
b LBB0_9
LBB0_2:
mov x8, #1 // index 초기화
str x8, [sp, #16]
b LBB0_3
- 함수 시작 부분에서 min_index가 초기화된다
- 그리고 LBB0_2 라벨 이후에 index가 초기화된다
즉, min_index가 탈출 조건 이전에 초기화된다.
결론
3가지 포인트를 분석해보았다.
- 변수가 선언 시점에 명령어로 접수되는지
- 변수가 선언했더라도, 사용(초기화) 시점에 명령어로 접수되는지
- 선언 뿐만 아니라 초기화까지 했더라도 실제로 사용하는 시점에 명령어로 접수되는지
1, 2의 경우는 동일한 어셈블리 코드가 작성되었고, 3의 경우는 달랐다.
정리하면 아래와 같다.
- 컴파일러에 의해서, 초기화 시점에 실제 명령이 접수된다.
- 따라서, 사용 직전에 초기화해 주자.
'최적화' 카테고리의 다른 글
[C언어] 컴파일러의 switch문 최적화 (0) | 2024.07.04 |
---|
- Total
- Today
- Yesterday
- PS
- Spring MVC
- generic sort
- reader-writer lock
- S1
- core c++
- tree
- Dispatcher Servlet
- C
- OOP
- Java
- generic swap
- pocu
- CPU
- 객체 변조 방어
- 연관관계 편의 메서드
- 개발 공부 자료
- 논문추천
- condition variable
- 엔티티 설계 주의점
- 이진탐색
- sleep lock
- servlet
- thread
- JPA
- S4
- Memory
- 톰캣11
- 백준
- tomcat11
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |