Swift Compiler #1

sanghoon Ahn·2021년 11월 27일
0

iOS

목록 보기
12/19
post-thumbnail

안녕하세요!!

이번에는 Compiler를 주제로 이야기를 해볼 건데요,

CS시간에 듣기만 했던 컴파일러.. Swift에서는 어떻게 동작하는지 궁금하지 않으셨나요 ???

그런 궁금증을 조금이나마 해소 시켜보기 위해 이번 주제는 Compiler로 선정했습니다. 👏👏👏👏

긴긴 이야기가 될 것 같지만 우리 끝까지 함께해요 ~~!!

The Swift Compiler

Swift 툴체인의 중심에는 Swift Compiler가 있습니다.

Swift Compiler의 책임은 소스코드를 실행할 수 있는 object 코드로 변경시킵니다.

Data Flow

Swift Compiler는 LLVM이라는 Compiler의 기본구조에서 동작하며, 다음과 같은 data flow를 가집니다.

Swift와 같은 high-level 언어를 machine code로 변환하는 과정은 lowering이라는 실제 하드웨어에서 효과적으로 동작합니다.

둥근 코너의 사각형과 일반 사각형은 데이터의 input과 output을 나타내며, high level에서부터 각각의 단계를 이해하는데 많은 도움을 줄것입니다.

  1. Parse: Swift 소스는 먼저 token들로 변환되고, AST(abstract syntax tree)에 들어가게 됩니다.

    각각이 노드로 표현되는 트리라고 생각 할 수 있습니다. 각 노드들은 소스들의 위치 정보를 함께 가지고 있기 때문에 에러를 찾았을 때 각 노드는 정확하게 어디에서 문제가 발생 했는지 말해줍니다.

    AST에 대한 자세한 내용은 문서에서 확인 하실 수 있습니다.

    💡 다음 내용을 설명하기 전에 짚고 넘어가야 할 토막 상식 Syntax(구문) / Semantics(의미)

  2. Sematic Analysis(Sema): 해당 단계에서 컴파일러는 AST를 사용하여 프로그램의 의미를 분석합니다.

    해당 단계에서 type checking이 일어납니다.

    마찬가지로 AST를 사용하기 때문에 에러가 발생한 위치를 정확하게 짚어 낼 수 있습니다.

    type check 완료 후 AST는 type-checked AST 상태가 되며, 이를 SILGen 단계로 전달하게 됩니다.

  3. SILGen: 이 단계는 해당 단계를 가지지 않은 Clang 처럼 이전 컴파일러 파이프라인에서 벗어납니다.

    AST는 ASL(Swift Intermediate Language)로 lowered됩니다. SIL은 basic block을 가지고있는데, 이는 Swift Type, RC, Dispatch rules들과 computation을 가지고 있습니다.

    SIL은 raw와 canonical 두가지 특색을 가지고 있습니다. Canonical SIL은 raw SIL의 최소한의 최적화를 통한(모든 최적화를 진행하지 않아도) 결과입니다.

    SIL은 또한 소스의 위치정보를 가지고 있기 때문에 의미있는 에러를 제공합니다.

  4. IRGen: 이 도구는 SIL을 LLVM의 Intermediate Representation으로 lower시킵니다. 이 시점의instructions는 Swift만의 특성이 아닙니다. (모든 LLVM 기반은 이 representation을 사용합니다)

    IR은 꽤 추상적입니다. SIL처럼 IR은 Static Single Assignment(SSA) 형태 입니다.

    IR은 무제한의 레지스터를 가진 machine처럼 모델링하고, 최적화를 찾기 쉽게 만듭니다.

    또한, Swift type과는 무관합니다.

  5. LLVM(Low Level Virtual Machine): 최종 단계는 IR을 최적화 하고, 특정 플랫폼의 machine의 명령어로 lower를 진행합니다.

    백엔드(machine 명령어를 방출하는것)에는 ARM, x86, Wasm 등이 포함됩니다.

    위의 다이어그램은 Swift 컴파일러가 어떻게 object code를 생성하는지 보여줍니다.

    source code formatters, refactoring tools, documentation generators and syntax highlighters과 같은 도구들도 AST와 같은 중간 결과를 활용하여 최종 결과를 보다 견고하고 일관되게 만들 수 있습니다.

    ⭐️ 나만의정리
    파싱을 통해 AST(Abstract Syntax Tree) 생성 →
    생성된 AST를 활용하여 의미를 분석하고, type check 진행 →
    AST를 활용하여 SIL(Swift Intermidate Language) 생성 →
    IR(Intermediate Representation) 생성 →
    LLVM에서 IR을 최적화 하고 특정 플랫폼의 기계 명령어로 변환

The magic of SIL

스위프트의 발전으로 소스 언어의 모든 형식 의미론을 유지하는 Intermediate Language를 만들겠다는 생각은 새로웠습니다.

특정한 진단 루틴을 보여주고, 높은 수준의 최적화를 수행하기 위해 극히 우회적인 루트를 취해야만하는 다른 LLVM 컴파일러들과 다르게 SILGen은 직접적으로 테스트 가능한 방법을 제공 할 수 있습니다.

Apple이 LLVM과 Clang을 Xcode의 컴파일러 기술에 적용하기 이전에는 syntax highlighting, document generation, debugging and compiling 모두 다른 parser를 사용했습니다.

꽤 오랫 동안 잘 동작했지만, 동기화 되지 않는다면 더 큰 문제가 발생할 수 있었기 때문에 새로운 Intermediate Language를 만들게 되었습니다.

Overflow Detection

SIL의 능력을 다음 동작에서 확인할수 있습니다. 다음 error를 봐주세요.

SILGen의 pass 덕분에 컴파일러는 source를 정적으로 분석(컴파일 타임에 체크)했고, 130은 127까지만 올라가는 Int8에 들어갈 수 없다는 것을 알 수 있습니다.

Definite initialization

Swift는 기본적으로 초기화 되지 않은 메모리에 접근하기 어렵기 때문에 안전한 언어입니다.

SILGen은 checking proccess를 통해 확실하게 initialization 호출 보장을 제공합니다.

final class Printer {
  var value: Int
  init(value: Int) { self.value = value }
  func print() { Swift.print(value) }
}

func printTest() {
  var printer: Printer
  if .random() {
    printer = Printer(value: 1)
  }
  else {
    printer = Printer(value: 2)
  }
  printer.print()
}

printTest()

위 코드는 컴파일과 실행 모두 문제 없습니다.

그러나, else구문을 주석 처리 한다면, 컴파일러는 SIL을 통해 (Variable ‘printer’ used before being initialized)라는 에러를 표시합니다.

이러한 에러가 가능한 이유는 SIL은 Printer의 메소드 호출 의미를 알고있기 때문입니다.

SIL은 Sementic Analysis 단계를 거쳤기 때문에 프로그램의 Sementic을 이해하고 있습니다!

Allocation and devirtualization

SILGen은 함수의 호출과 allocations의 최적화를 지원합니다.

class Magic {
  func number() -> Int { return 0 }
}

final class SpecialMagic: Magic {
  override func number() -> Int { return 42 }
}

public var number: Int = -1

func magicTest() {
  let specialMagic = SpecialMagic()
  let magic: Magic = specialMagic
  number = magic.number()
}

이 코드는 number를 설정하는데 있어 가장 이상한 코드일 것입니다.

magicTest 함수에서 SpecialMagic instance를 생성하고, base class에 할당합니다.

그리고 number()함수를 호출하여 결과값을 전역 number 에 설정합니다.

개념상 vtable을 사용하여 42를 return 하는 올바른 함수를 찾기 때문에, 42가 설정됩니다.

Raw SIL

terminal에서 magic.swift가 있는 디렉토리로 이동해서 다음 명령어를 실행하면

swiftc -O -emit-silgen magic.swift > magic.rawsil

Swift 최적화와 raw SIL을 생성하는 컴파일러를 실행하고, magic.rawsil 파일이 생성됩니다.

rawsil 파일은 매우 복잡한 텍스트로 이루어져 있기 때문에 놀라지 마세요!

아래쪽에 magicTest 함수에 대한 정의를 찾을 수 있습니다.

// magicTest()
sil hidden [ossa] @$s5magic0A4TestyyF : $@convention(thin) () -> () {
bb0:
  %0 = global_addr @$s5magic6numberSivp : $*Int   // user: %14
  %1 = metatype $@thick SpecialMagic.Type         // user: %3
  // function_ref SpecialMagic.__allocating_init()
  %2 = function_ref @$s5magic12SpecialMagicCACycfC : $@convention(method) (@thick SpecialMagic.Type) -> @owned SpecialMagic // user: %3
  %3 = apply %2(%1) : $@convention(method) (@thick SpecialMagic.Type) -> @owned SpecialMagic // users: %18, %5, %4
  debug_value %3 : $SpecialMagic, let, name "specialMagic" // id: %4
  %5 = begin_borrow %3 : $SpecialMagic            // users: %9, %6
  %6 = copy_value %5 : $SpecialMagic              // user: %7
  %7 = upcast %6 : $SpecialMagic to $Magic        // users: %17, %10, %8
  debug_value %7 : $Magic, let, name "magic"      // id: %8
  end_borrow %5 : $SpecialMagic                   // id: %9
  %10 = begin_borrow %7 : $Magic                  // users: %13, %12, %11
  %11 = class_method %10 : $Magic, #Magic.number : (Magic) -> () -> Int, $@convention(method) (@guaranteed Magic) -> Int // user: %12
  %12 = apply %11(%10) : $@convention(method) (@guaranteed Magic) -> Int // user: %15
  end_borrow %10 : $Magic                         // id: %13
  %14 = begin_access [modify] [dynamic] %0 : $*Int // users: %16, %15
  assign %12 to %14 : $*Int                       // id: %15
  end_access %14 : $*Int                          // id: %16
  destroy_value %7 : $Magic                       // id: %17
  destroy_value %3 : $SpecialMagic                // id: %18
  %19 = tuple ()                                  // user: %20
  return %19 : $()                                // id: %20
} // end sil function '$s5magic0A4TestyyF'

단 세줄의 magicTest 함수 정의의 SIL 텍스트 입니다.

bb0 라벨은 basic block 0의 약자이며, computation의 단위 입니다.

(if/else 구문을 가진다면, bb1, bb2 등의 블록이 생성됩니다)

%1, %2 등은 virtual register의 값 입니다. SIL은 SSA(Single Static Assignment)이기 때문에 reigster는 무제한이며, 절대 재사용되지 않습니다.

위 텍스트를 읽으면 어떻게 objectse들을 allocating, assinging, calling 그리고 deallocating 하는지 알수 있으며, Swift언어로 작성된 코드의 모든 의미를 텍스트로 표현한 것 입니다.

Canonical SIL

Canonical SIL은 최소의 optimization pass set을 가진 최적화를 포함하고 있는데, 이 최적화는 -Onone 옵션에 의해 비활성화 됩니다.

다음 명령어를 터미널에서 실행하면

swiftc -O -emit-sil magic.swift > magic.sil

canonical SIL을 가지고 있는 maigic.sil 파일을 생성하며, 마찬가지로 아래쪽에서 magicTest 함수에 대한 정의를 확인 할 수 있습니다.

// magicTest()
sil hidden @$s5magic0A4TestyyF : $@convention(thin) () -> () {
bb0:
  %0 = global_addr @$s5magic6numberSivp : $*Int   // user: %3
  %1 = integer_literal $Builtin.Int64, 42         // user: %2
  %2 = struct $Int (%1 : $Builtin.Int64)          // user: %4
  %3 = begin_access [modify] [dynamic] [no_nested_conflict] %0 : $*Int // users: %4, %5
  store %2 to %3 : $*Int                          // id: %4
  end_access %3 : $*Int                           // id: %5
  %6 = tuple ()                                   // user: %7
  return %6 : $()                                 // id: %7
} // end sil function '$s5magic0A4TestyyF'

raw SIL보다는 많이 간결해졌지만, 의미 하는것은 똑같습니다.

주된 작업은 integer literal 42(%2)를 global address location(%3)에 저장하는것입니다.

class들은 initialized되거나 de-initialized되지 않습니다. virtual method들 또한 호출되지 않습니다.

sturcture들은 stack, class들은 heap메모리를 사용 한다는것을 알고있지만, 이것은 일반화라는것을 명심해야 합니다.

Swift에서는 모든것이 heap 위에서 initialized 되고, SIL anlaysis는 allocation을 stack 에서 진행하도록하거나 완전히 제거 할 수 있습니다.

Virtual function 호출들은 또한 최적화 process와 직접 혹은 inlined 호출을 통해 devirturalized 될 수 있습니다.

Implementing a language feature

Swift는 컴파일러에서 라이브러리로 가능한 많은 기능들을 넣었습니다.

Optional 이 단순한 generic enumration이라는것도 알고 있을 겁니다.

대부분의 기본적인 타입들은 standard library의 일부분이며, 컴파일러 내에 내장되어있지 않습니다.

BoolIntDoubleStringArraySetDictionaryRange 등등을 포함합니다.

Swift의 좀 더 난해한 특징들에 대해 배우거나 기본적인 특징들을 더 잘 이해할 수 있는 좋은 방법은 스스로 작성해 보는 것이라고 하네요.

글이 너무 길어질 것 같아서 2부에서 함께 작성해보면서 이해 해보도록 하죠!!

마무리 하며..

이번글은 Swift Compiler가 어떤구조로 이루어져있는지, 어떤식으로 동작하는지 간단하게 살펴보았습니다.

다음포스트는 언제가될지 모르겠지만... 이어서 작성해보도록 하겠습니다.

2021년도 거의 마무리 되어가네요!!
한달에 한가지의 글을 포스팅 해보자는 첫 해의 다짐을 끝까지 잘 마무리 할 수 있도록 노력해보겠습니다. 🙏

질문이나 지적은 언제든 환영입니다!!

읽어주셔서 감사합니다 ! 🙇🏻‍♂️

  • 해당 글은 RayWenderRich의 expert-swift 책을 참고하여 작성하였습니다.
profile
hello, iOS

0개의 댓글