
SIL은 Swift 컴파일 과정에서 가장 독특하면서도 중요한 부분이에요. 일반적인 컴파일러 (C, C++ 등) 가 소스코드에서 바로 LLVM IR로 넘어가는 것과는 달리, Swift는 그 사이에서 SIL이라는 단계를 하나 더 두었어요.
왜 굳이 일을 두 번 할까요? 그 이유는 “Swift는 너무 똑똑하고 안전한 언어라서” 그래요.
LLVM IR은 하드웨어에 가깝기 때문에, Swift가 가진 고수준의 언어적 특징들을 다 이해하지 못해요. 만약 SIL 없이 바로 LLVM IR로 간다면 다음과 같은 문제가 발생해요.
즉, SIL은 Swift의 언어적 의미를 유지하면서 최적화를 수행하기 위한 중간 기착지에요.
SIL은 컴파일 과정 중에 두 가지 모습으로 변해요.
Swift 문법을 막 분석해서 나온 상태에요. 여기서는 “데이터 흐름 분석(Data Flow Analysis)”을 통해 다음을 검사해요.
검사가 끝나면 Swift 특유의 성능 최적화를 진행해요.
retain / release 호출을 찾아내서 제거해요. (메모리 관리 효율화의 작업이에요.)이해를 돕기 위해 아주 간단한 Swift 코드가 SIL에서 어떻게 보이는지 확인해봐요.
// Swift Code
func add(a: Int, b: Int) -> Int {
return a + b
}
이 코드가 SIL로 변하면 대략 이런 식이 돼요. (실제보다 단순화한 버전이에요.)
// SIL Code (의사 코드)
sil hidden @add_function : $@convention(thin) (Int, Int) -> Int {
bb0(%0 : $Int, %1 : $Int):
%2 = struct_extract %0 : $Int, #Int._value // 정수 구조체에서 실제 값을 추출
%3 = struct_extract %1 : $Int, #Int._value
%4 = builtin "add_Int64"(%2 : $Builtin.Int64, %3 : $Builtin.Int64) // 내장 덧셈 수행
%5 = struct $Int (%4 : $Builtin.Int64) // 다시 Int 구조체로 포장
return %5 : $Int // 반환
}
bb0 (Basic Block): 실행의 기본 단위에요.$Int : Swift의 Int 가 단순한 숫자가 아니라 struct 임을 인지하고 있어요.SIL이 주는 이점
직접 확인해보고 싶어서 터미널에서 swiftc -emit-sil 파일명.swift 를 입력하여 SIL로 변환을 해보았어요.
import Foundation
public enum ViewType {
case signIn
case myTamin
}
변환을 시도한 코드에요. enum으로 화면의 타입을 지정해놓은 코드에요.
그런데, 변환을 시도하니 다음과 같이 나왔어요.
실제 코드는 너무 길어서 엄청 축약을 했습니다.
import Builtin
import Swift
import SwiftShims
import Foundation
public enum ViewType {
case signIn
case myTamin
@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: ViewType, _ b: ViewType) -> Bool
func hash(into hasher: inout Hasher)
var hashValue: Int { get }
}
// main
// Isolation: unspecified
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2) // user: %4
return %3 // id: %4
} // end sil function 'main'
// static ViewType.__derived_enum_equals(_:_:)
// Isolation: unspecified
sil @$s8ViewTypeAAO21__derived_enum_equalsySbAB_ABtFZ : $@convention(method) (ViewType, ViewType, @thin ViewType.Type) -> Bool {
// %0 "a" // users: %7, %3
// %1 "b" // users: %22, %4
// %2 "self" // user: %5
bb0(%0 : $ViewType, %1 : $ViewType, %2 : $@thin ViewType.Type):
debug_value %0, let, name "a", argno 1 // id: %3
debug_value %1, let, name "b", argno 2 // id: %4
debug_value %2, let, name "self", argno 3 // id: %5
%6 = alloc_stack [var_decl] $Int // users: %16, %10, %41
switch_enum %0, case #ViewType.signIn!enumelt: bb1, case #ViewType.myTamin!enumelt: bb2 // id: %7
bb1: // Preds: bb0
%8 = integer_literal $Builtin.Int64, 0 // user: %9
%9 = struct $Int (%8) // users: %11, %13
%10 = begin_access [modify] [static] %6 // users: %11, %12
store %9 to %10 // id: %11
end_access %10 // id: %12
br bb3(%9) // id: %13
bb2: // Preds: bb0
%14 = integer_literal $Builtin.Int64, 1 // user: %15
%15 = struct $Int (%14) // users: %17, %19
%16 = begin_access [modify] [static] %6 // users: %17, %18
store %15 to %16 // id: %17
end_access %16 // id: %18
br bb3(%15) // id: %19
// %20 // user: %36
bb3(%20 : $Int): // Preds: bb1 bb2
%21 = alloc_stack [var_decl] $Int // users: %31, %25, %40
switch_enum %1, case #ViewType.signIn!enumelt: bb4, case #ViewType.myTamin!enumelt: bb5 // id: %22
// ...
// Mappings from '#fileID' to '#filePath':
// 'ViewType/ViewType.swift' => '/Users/tabber/Desktop/github/MiTamin/MiTamin/Source/Extension/ViewType.swift'
소스코드는 단 4줄짜리 enum 인데, SIL은 수백 줄에 달했어요.
그리고 이 현상이 “Swift가 왜 안전하고 강력한 언어인가”를 보여주는 증거에요.
왜 그런지에 대해 더 알아보았어요.
제가 만든 코드인 ViewType 파일은 단순해 보이지만, Swift 컴파일러는 이 enum 이 나중에 Equatable 이나 Hashable 처럼 독장할 수 있도록 기본적인 뼈대 코드를 자동으로 생성해요.
__derived_enum_equals : enum 의 케이스들을 서로 비교하기 위한 로직이에요. SIL 코드를 보면 swift_enum 을 통해 signIn 인지 myTamin 인지 일일히 대조하는 복잡한 분기문이 생성된 것을 볼 수 있어요.hash(into:) & hashValue : 이 enum 이 딕셔너리의 키로 쓰이거나 Set에 담길 때 필요한 해시 계산 로직이에요.개발자가 작성하지 않아도 컴파일러가 “너 나중에 이거 쓸 거지? 그럼 내가 미리 안전하게 만들어둘게”라며 정교한 코드를 덧붙이는 과정이에요.
SIL 코드에서 @$s8ViewTypeAAO21__derived_enum_equalsySbAB_ABtFZ 같은 괴상한 이름들을 봤을거에요.
이를 Name Mangling 이라고 해요.
Swift는 오버로딩(이름은 같지만 파라미터가 다른 함수)을 지원하기 때문에, 컴파일러 레벨에서는 함수 이름 + 파라미터 타입 + 리턴 타입 + 모듈 이름을 모두 섞어서 세상에 단 하나뿐인 고유한 ID를 만들어요. 나중에 링크 단계에서 정확한 함수를 찾아가기 위한 주소표라고 생각하시면 돼요.
SIL의 구조를 다시 보면 bb0, bb1 , bb2 같은 Basic Blocks 로 나뉘어 있어요.
alloc_stack / dealloc_stack : Swift는 메모리 안정성을 위해 스택 메모리 할당과 해제를 명확하게 기록해요.begin_access / end_access : 독점적 메모리 접근 (Exclusive Access to Memory) 을 검사하는 코드에요. 여러 곳에서 동시에 값을 수정하려 할 때 런타임 에러를 내주는 그 안전 장치가 SIL 레벨에서 이미 삽입돼요.출력된 SIL 중에서 가장 중요한 부분은 하단의 sil_witness_table 이에요.
이 Witness Table은 Swift 다형성의 핵심이에요.
“ViewType이라는 애는 Equatable 프로토콜을 만족하려면 이 함수 주소로 가면 돼!” 라는 매핑 테이블이에요. 여기서 PWT라는 개념이 나오는데, Protocol Witness Table (PWT)에 대해서는 클릭하시면 자세히 정리가 되어있어요.
우리가 Swift로 “이거 해줘” 라고 한마디 하면 SIL은 그걸 하기 위해서 “메모리는 어떻게 잡고, 타입은 어떻게 하고, 에러는 어떻게 처리할지”를 아주 상세하게 적어놓은 실행 명세서와 같아요.
해당 내용을 공부하면서 든 궁금증을 다시 공부해보기로 했어요.
질문 내용은 다음과 같았어요.
아까 "강력한 타입 : 모든 단계에 타입 정보가 붙어 있어 안전합니다." 이 구문에 대한 질문인데, 이 모든 타입이 붙어있다는 점 때문에 Swift은 타입 추론에 강점이 있고 안전한 언어라고 하는거야?
내용이 얼추 정확 합니다. 하지만 ‘타입 추론’과 ‘타입 안전’을 나누어서 이해하면 더 명확해져요.
let x = 10 이라고만 써도 컴파일러가 “아, 이건 Int 구나”라고 판단하는 과정이죠.결론적으로 타입 추론 덕분에 코딩은 편해지고, SIL에서 타입을 엄격히 고정하기 때문에 런타임에 타입으로 인한 사고가 안 터지는 ‘안전한 언어’가 되는 것이에요.
EXC_BAD_ACCESS질문 내용은 다음과 같았어요.
begin_access / end_access 독점적 메모리 접근을 설명할 때 "런타임 에러를 내주는 그 안전장치가 SIL 레벨에서 이미 삽입"된다고 했잖아. 이게 소위 크래시 상황 중 BAD_ACCESS 상황이야? 그리고 이걸 어떻게 도출되게 하는지도 궁금해
이건 제가 혼동한 부분입니다. 아주 중요한 차이가 있어요.
begin_access 와 Law of Exclusivity
Swift는 “한 변수를 동시에 수정하거나, 수정하는 도중에 읽는 행위”를 엄격히 금지해요.
begin_access / end_access ) : 정적으로 판단이 안되는 경우 (ex: 글로벌 변수, 클래스 프로퍼티 등) SIL 단계에서 검사 코드를 끼워 넣어요.EXC_BAD_ACCESS와의 차이
begin_access 실패 : Swift 언어 차원에서 “야, 너 지금 데이터 레이스 날 수 있어! 위험해!” 라고 의도적으로 앱을 죽이는 안전한 크래시에요.EXC_BAD_ACCESS : 이건 보통 메모리 주소 자체가 잘못되었을 때 발생해요. 이미 해제된 메모리 (댕글링 포인터) 에 접근하거나, 아예 권한이 없는 메모리 번지를 찌를 때 OS가 던지는 에러에요. Swift는 ARC와 강력한 포인터 제어를 통해 이 EXC_BAD_ACCESS 를 최대한 예방하려 노력해요.