on
ARC(Automatic Reference Counting)를 SIL로 직접 확인해보기
ARC(Automatic Reference Counting)를 SIL로 직접 확인해보기
1. Swift의 ARC, 직접 눈으로 볼 수 있을까요?
Swift 개발을 하다 보면 “ARC는 인스턴스 생성 시 retain, 해제 시 release”라고 배우는 경우가 많아요.
하지만 실제로 컴파일 타임에 어떤 ARC 코드가 삽입되는지 직접 본 적 있으신가요?
오늘은 Swift의 ARC가 컴파일 타임에 어떻게 처리되는지,
그리고 우리가 작성한 코드가 SIL(Swift Intermediate Language)로 변환됐을 때
retain/release가 실제로 어떻게 동작하는지 직접 확인하는 방법을 소개해드릴게요.
ARC가 무엇인지에 관한 설명은 이미 좋은 글들이 많아서 생략합니다 :)
2. SIL 출력하는 방법
Swift 컴파일러(swiftc)는 -emit-sil 옵션을 통해 소스 코드를 SIL로 변환한 중간 결과를 볼 수 있습니다.
swiftc -emit-sil main.swift
3. 예제 코드
class Person { }
func foo() {
var a: Person? = Person()
var b: Person? = a
b = nil
a = nil
}
4. SIL로 확인하는 ARC의 실제 동작
이 코드를 SIL로 컴파일해보면, ARC가 어떻게 retain/release를 삽입하는지 눈으로 직접 볼 수 있어요.
주요 포인트
4-1. 객체 생성 시점
%0 = alloc_stack [lexical] [var_decl] $Optional<Person>, var, name "a", type $Optional<Person> // users: %6, %24, %23, %16
%1 = metatype $@thick Person.Type // user: %3
// function_ref Person.__allocating_init()
%2 = function_ref @$s1c6PersonCACycfC : $@convention(method) (@thick Person.Type) -> @owned Person // user: %3
%3 = apply %2(%1) : $@convention(method) (@thick Person.Type) -> @owned Person // user: %4
%4 = enum $Optional<Person>, #Optional.some!enumelt, %3 : $Person // users: %8, %6, %5
store %4 to %0 : $*Optional<Person> // id: %6
이 시점에 retain 관련 코드가 없는 것을 알 수 있는데 여러 자료를 찾아보았지만 명확한 답은 찾지 못했고, 예상할 수 있는 지점이 있어요.
Swift RefCount 이 문서를 보면 when the physical field is 0 the logical value is 1
라는 부분이 있어요.
이 부분을 통해서 예상해보자면 init 호출 시점에 reference count가 0으로 (physically) 초기화 되어서 logically 1이 되는 것이 아닐까..? 하고 추측하고 있어요.
4-2. 값 복사(b = a) 시점
retain_value %4 : $Optional<Person> // id: %5
%7 = alloc_stack [lexical] [var_decl] $Optional<Person>, var, name "b", type $Optional<Person> // users: %8, %22, %21, %10
store %4 to %7 : $*Optional<Person> // id: %8
retain_value
를 통해서 retain 코드가 실행되고, %4 (변수 a)의 값이 변수 b로 저장돼요.
4-3. Optional을 nil로 변경(해제)할 때
%9 = enum $Optional<Person>, #Optional.none!enumelt // user: %12
%10 = begin_access [modify] [static] %7 : $*Optional<Person> // users: %12, %11, %14
%11 = load %10 : $*Optional<Person> // user: %13
store %9 to %10 : $*Optional<Person> // id: %12
release_value %11 : $Optional<Person> // id: %13
end_access %10 : $*Optional<Person> // id: %14
%15 = enum $Optional<Person>, #Optional.none!enumelt // user: %18
%16 = begin_access [modify] [static] %0 : $*Optional<Person> // users: %18, %17, %20
%17 = load %16 : $*Optional<Person> // user: %19
store %15 to %16 : $*Optional<Person> // id: %18
release_value %17 : $Optional<Person> // id: %19
end_access %16 : $*Optional<Person> // id: %20
release_value
가 두 번 호출되는 것을 알 수 있어요.
결론
SIL을 직접 뜯어보면 우리가 흔히 “생성 시 retain이 무조건 들어간다”라고 생각했던 부분도 실제로는 Reference Count가 0(물리값)에서 시작해 논리적으로 1로 간주된다는 점, 그리고 값 복사, 해제 시점에 retain/release가 삽입되는 구조임을 알 수 있었네요.