티스토리 뷰

들어가며

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. 선언 뿐만 아니라 초기화까지 했더라도 실제로 사용하는 시점에 명령어로 접수되는지

1, 2의 경우는 동일한 어셈블리 코드가 작성되었고, 3의 경우는 달랐다.

 

정리하면 아래와 같다.

  • 컴파일러에 의해서, 초기화 시점에 실제 명령이 접수된다.
  • 따라서, 사용 직전에 초기화해 주자.

 

 

'최적화' 카테고리의 다른 글

[C언어] 컴파일러의 switch문 최적화  (0) 2024.07.04
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함