• 앱 만드는 과정: 코드 - 컴파일 - 실행
    • 컴파일 까지는 Xcode 및 Swift 컴파일러와 Clang이 하지만, 실제 실행은 Swift와 Objective-C 런타임의 역할이 크다.
  • 런타임은 애플 플랫폼에 내장되어서, 컴파일러가 할 수 없는 일을 실행시에 해준다.
  • 이번 세션은 API 변경 없고, 빌드 세팅 변경도 없다. 모든 변화는 모든 프로그램에 동일하게 적용된다.
  • 변경사항
    • Protocol Checks
      • 예시 코드

        protocol CustomLoggable {
        	var customLogString: String
        }
        
        func log(value: Any) {
        	if let value = value as? CustomLoggable { 
        		...
        	}
        }
        
        struct Event: CustomLoggable {
        	var name: String
        	var date: Date
        
        	var customLogString: String {
        		return "\\(self.name), on \\(self.date)"
        	}
        }
        
        let event = ...
        
        log(value: event)
        
        
      • 타입 체크 연산(as, is)는 가능하면 컴파일타임에 최적화되지만, 항상 가능한 건 아니여서 런타임에 일어날 수도 있다.

        • 미리 계산된 프로토콜 메타데이터를 이용해서 특정 개체가 진짜 그 프로토콜을 채택하고 있는지 알 수 있다.
        • 이 메타데이터는 컴파일 타임에 대부분 빌드되지만, 제네릭을 쓰는 경우 등에는 런치 타임에 계산되어야 한다. → 실제 앱에서 전체 런치 타임에 절반 정도를 여기서 소모하기도 한다.
        • 새로운 런타임은 이를 dyld closure를 만들 때 미리 계산한다.
        • 새 OS에서 실행되는 기존 앱에도 적용된다.
    • Message Send
      • ARM 64에서 message send 호출을 12바이트 → 8바이트로 줄임

        • 왜 이렇게 크지? 이는 objc_msgsend를 호출하기 전, 적절한 selector를 준비하는 과정이 추가로 붙기 때문
        • selector준비가 8바이트, 실제 호출이 4바이트
          • 이 과정은 동일한 메소드를 호출해도 반복적으로 일어나는데, 이 selector 준비를 별도 메소드로 묶어준다.(헬퍼 함수)

          • msgSend도 한번 더 indirection해준다.

          • 근데 이렇게 하면 크기는 줄지만 indirection이 두번 일어나니까 성능상으로 좋지 않다. 그래서 두개의 stub을 합쳐준다.

            스크린샷 2022-06-10 오전 11.43.08.png

      • 전체적으로 바이너리가 2% 정도 작아짐

      • Xcode 14로 빌드하면 적용됨 → 기존 OS에서 돌려도 적용되는 사항

        • 기본적으로는 성능과 크기 밸런스를 맞춰줌
        • 옵션으로 크기 최적화만 하도록 할 수도 있음(-Wl, -objc-stubs_small 링커 플래그)
    • Retain and Release
      • Retain과 Release연산이 ARM64기준 8바이트 → 4바이트로 줄어든다.
      • 바이너리 크기가 2%정도 줄어드는 효과가 있다.
      • 런타임 지원이 필요하기 때문에 deployment target을 iOS 16, tvOS 16, WatchOS 9으로 맞춰야 한다.
      • 원리
        • ARC에서는 objc_retain, objc_release를 자동으로 삽입해준다.
        • 이 두 함수는 일반적인 C함수로 구현되어 있고, 개체 포인터 한개를 인자로 받는다.
        • C함수기 때문에 C 함수 호출 규약을 지켜야하고, 이 때문에 추가적인 연산이 들어간다.
          • 객체 포인터를 적절한 레지스터로 옮겨야 한다(mov)
        • 그래서 해당 함수에 대해서는 커스텀 호출 규약을 적용함으로써 이미 포인터가 있는 위치를 정확하게 지정할 수 있게 되어서 mov 연산이 필요없어진다.
    • Autorelease elision
      • objc 런타임의 변화로, autorelease elision이 빨라졌다. → 새로운 OS에서 돌아가는 기존 앱도 혜택을 받는다.
      • 추가로 타겟을 iOS 16 계열로 지정하면 코드 사이즈도 줄어든다.
      • autorelease elision?
        • ARC가 대부분 잘해주지만, 여전히 autorelease가 필요한 상황은 있다.
          • ex) 함수에서 로컬 인스턴스를 반환하는 경우, 바로 release되면 안되기 때문에 autorelease로 처리한다.
        • autorelease는 release 시점을 미루기 위한 테크닉이다.
        • 일반적으로는 언제 해제될지 런타임이 보장할 수 없지만, 리턴으로 다른 변수에 할당하는 경우느 바로 리테인을 하기 때문에 컴파일러가 처리해줄 수 있다.
        • autorelease에는 오버헤드가 있다. 여기서 autorelease elision이 나온다.
          • autorelease 반환값이 있는 경우, 컴파일러는 여기서만 쓰이는 특별한 마커를 런타임에 내보낸다.
          • 이후 실행되는 retain에서 런타임은 받았던 마커를 로드해서 retain하는 인스턴스와 비교한다.
            • 이 때 일치하면 쌍이되는 autorelease와 retain을 한꺼번에 없앨 수 있다.(이미 처음 만들 때 retain을 내부에서 했으니, release만 없으면 count은 여전히 1일테니까) 이것이 autorelease elision이다.
      • autorelease elision을 처리하는 것 자체도 비용이 든다.
        • 코드 자체를 데이터로 로딩해야 하기 때문에, 일반적인 상황이 아니라 CPU에 최적화 되어있지 않다.
        • 그래서 반환 주소값을 대신 이용하면 단순 포인터 비교라서 더 쉽게 확인할 수 있다.
        • 특별한 마커도 필요없기 때문에 안 쓸 수 있다.