스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
앱 크기 및 런타임 성능 향상
앱의 크기를 줄이면서 더 빠르게 작동하고 실행할 수 있도록 Swift 및 Objective-C 런타임을 어떻게 최적화했는지 알아보세요. Xcode 14로 앱을 빌드하고 배포 대상을 업데이트할 때 효율적인 프로토콜 검사, 소규모 메시지 전송 호출 및 최적화된 ARC에 액세스할 수 있는 방법을 살펴보세요.
리소스
관련 비디오
WWDC22
-
다운로드
♪ ♪
안녕하세요, 저는 Clang 및 Swift 컴파일러를 담당하는 Ahmed입니다 이 세션에서는 일반적 Swift 및 Objective-C 작업을 더 빠르고 효율적으로 만들기 위한 변경 사항 들에 대해 자세히 살펴보겠습니다 앱의 크기와 런타임 성능을 개선할 수 있죠 Swift 또는 Objective-C에 코드를 작성하는 것은 실제로는 두 가지 주요 컴포넌트와 상호작용하는 겁니다 Xcode를 사용해 빌드하고 이는 Swift 및 Clang 컴파일러를 사용합니다 그러나 앱을 실행할 때는 많은 무거운 작업들이 Swift 및 Objective-C 런타임에서 수행되죠 모든 플랫폼의 운영 체제에는 런타임이 내장되어 있습니다 컴파일러가 빌드 타임에 하지 못하는 일은 런타임이 런타임에 합니다 그럼 컴파일러와 런타임 이 둘 모두에 대한 개선 사항들을 살펴보겠습니다 이번 세션은 조금 특이합니다 새로운 API도 언어 변경이나 새로운 빌드 설정도 없죠 코드를 변경할 필요가 없기 때문에 모든 개선 사항이 투명하게 개발자에게 보여집니다 그럼 뛰어들어볼까요 네 가지 개선 사항을 살펴볼겁니다 우리는 Swift의 프로토콜 검사를 더 효율적으로 만들었습니다 또한 Objective-C 메세지 전송 호출을 더 작게 만들었죠 retain/release 호출도 마찬가지입니다 마지막으로 자동 릴리스 제거를 더 빠르고 더 작게 만들었습니다 그럼 자세히 살펴보겠습니다
Swift의 프로토콜 검사부터 시작하죠
여기 CustomLoggable이라는 프로토콜이 있습니다 여기에는 읽기 전용 계산 속성 customLogString이 있으며 CustomLoggable 객체에 대한 특수 처리가 있는 log 함수에서 이를 사용할 수 있죠 나중에 이름 및 날짜 필드가 있는 Event 타입을 정의합니다 customLogString 속성에 대한 getter를 정의하여 CustomLoggable 프로토콜을 준수하고 있죠
이렇게 하면 Event 객체를 'log' 함수에 전달할 수 있습니다 이 코드를 실행할 때 'log' 함수는 우리가 전달한 값이 프로토콜을 준수하는지 확인합니다 'as' 연산자를 사용하여 수행하죠 'is' 연산자를 본 적이 있을 겁니다
가능할 때마다 이 검사는 빌드 시에 컴파일러에서 최적화됩니다 하지만 아직 항상 충분한 정보가 있는 것은 아니죠 따라서 종종 최적화는 런타임에서 이루어져야 합니다 이전에 계산한 프로토콜 검사 메타 데이터의 도움을 받아서요 이 메타 데이터로 런타임은 이 특정 객체가 실제로 프로토콜을 준수하는지 여부를 알고 검사 확인이 성공합니다
메타 데이터의 일부는 컴파일 타임에 빌드되지만 많은 부분은 시작 시에만 빌드할 수 있습니다 특히 Generics를 사용할 경우에요 많은 프로토콜을 사용하는 경우 최대 수백 밀리초가 추가될 수 있죠 실제 앱에서는 시작 시간의 최대 절반이 걸리는 것을 보았습니다 새로운 Switf 런타임을 통해서는 앱 실행 파일과 시작 시 사용하는 모든 dylib에 대한 dyld 클로저의 일부로 이를 미리 계산합니다 무엇보다도 이 기능은 기존 앱 iOS 16, tvOS 16 또는 watchOS 9에서도 실행할 때 활성화됩니다 dyld 및 클로저에 대해 자세히 알고 싶다면 '앱 시작 시간: 과거, 현재 그리고 미래' 강연을 시청하세요 지금까지 Swift의 프로토콜 검사였습니다
이제 메시지 전송으로 넘어가 보죠 Xcode 14에서 새로운 컴파일러 및 링커를 사용하면서 메세지 전송 호출을 ARM64에서 12바이트에서 최대 8바이트 더 작게 만들었습니다 잠시 후 보겠지만 메세지 전송은 도처에 널려 있기 때문에 합치면 쌓입니다 우리는 바이너리에서 최대 2%의 코드 사이즈 향상을 보았습니다 이는 Xcode 14로 빌드 하면 자동으로 향상됩니다 배포 대상으로 이전 OS 릴리즈를 사용하는 경우에도 말이죠 성능과 사이즈의 균형을 이룬 최적화가 기본적으로 이루어지지만 objc_stubs_small 링커 플래그를 사용하여 사이즈만 최적화하도록 선택할 수 있습니다 이제 변경된 사항을 살펴봅시다 예를 들면서 시작하죠 컨퍼런스의 시작일에 대한 NSDate를 생성하려고 합니다 먼저 NSCalendar를 만드는 것으로 시작하고 NSDateComponents를 채우고 이로부터 날짜를 만들고 이를 반환합니다 이제 컴파일러가 생성하는 어셈블리를 살펴보죠 어셈블리의 세부 사항은 그다지 중요하지 않습니다 여러분이 그럴 필요 없도록 우리 컴파일러 담당자들이 주시하니까요 중요한 것은 여기 있는 거의 모든 라인들에서 objc_msgSend를 호출하라는 명령이 필요하다는 점입니다 날짜 컴포넌트에 대해서 하는 것 처럼 속성 액세스를 수행할 때도요 이는 컴파일 시에는 어떤 메서드를 호출해야하는지 모르고 objc 런타임에서만 호출할 수 있기 때문입니다 따라서 objc_msgSend를 사용하여 런타임을 호출해 올바른 메서드를 찾도록 요청하죠 이런 호출 중 하나에 집중해봅시다 objc_msgSend라는 호출 명령은 이미 언급했죠 하지만 더 있습니다 런타임에 어떤 메서드를 호출할지 알려주려면 이 objc_msgSend 호출에 셀렉터를 전달해야 합니다 셀렉터를 준비하려면 몇 가지 명령이 더 필요합니다 바이너리를 보면 각각의 명령은 공간을 차지합니다 ARM64에서는 각각 4바이트입니다 따라서 각 objc_msgSend 호출은 12바이트를 사용합니다 그리고 이는 매 호출마다 필요하죠 꽤나 쌓이게 되죠 이를 개선하기 위해 우리가 할 수 있는 일을 살펴봅시다
이전에 본 것 처럼 이 중 8바이트는 셀렉터를 준비하는데 사용됩니다 흥미로운 것은 어떠한 셀렉터여도 항상 동일한 코드라는 겁니다 따라서 이곳이 최적화의 시작점이 됩니다 항상 동일한 코드이기 때문에 공유할 수 있고 셀렉터당 한 번만 방출할 수 있죠 메세지 전송 때마다 매번 하는 대신에요 이 코드를 꺼내서 작은 도우미 함수에 넣은 다음 이 함수를 대신 호출합니다 동일한 셀렉터를 사용하는 많은 호출에 대해 모든 명령 바이트를 아낄 수 있습니다 이 도우미 함수를 '셀렉터 스텁'이라고 부릅니다
여전히 objc_msgSend 함수는 실제로 호출해야 하죠 따라서 계속 이어갑니다 그러면 또 다른 간접 참조가 있습니다 함수 자체의 주소를 로드하고 호출하죠 세부 사항은 중요하지 않지만 중요한 것은 이를 위해서 몇 바이트의 코드가 더 필요하는 겁니다
이때 제가 앞서 언급했듯이 원하는 모드를 선택할 수 있죠 여기서 했던 것처럼 이 두 개의 작은 스텁 함수를 별도로 유지해서 최대한 많은 코드를 공유하고 이 함수들을 가능한 작게 만듭니다 하지만 이렇게 하면 두 번 연속 호출이 수행되므로 성능에 이상적이지 않죠 대체 버전으로 개선할 수 있습니다 생성한 이 두 스텁 함수를 하나로 결합하는 것이죠 코드를 서로 가깝게 유지하면서 많은 호출이 필요하지 않게 됩니다 화면의 오른쪽 처럼요
그래서 이렇게 두 가지 옵션이 있습니다 사이즈만을 최적화할지 여부를 선택해주고 최대 사이즈 절감 효과를 얻을 수 있는데요 -objc_stubs_small 링커 플래그를 사용하거나 사이즈 이점을 제공하는 코드 생성을 사용해 최상의 성능을 유지할 수도 있죠 사이즈의 제약이 심하지 않은 한 오른쪽 사용을 권장하며 이것이 기본 값입니다 지금까지 스텁을 사용한 더 작은 메세지 전송이었습니다 또 다른 개선 사항은 더 저렴해진 유지/릴리스 비용입니다 Xcode 14의 새로운 컴파일러 사용은 ARM64에서 유지/릴리스 호출이 8바이트에서 최대 4바이트 작아졌습니다 잠시 후 살펴보겠지만 메세지 전송과 마찬가지로 유지/릴리스 또한 어디에서나 볼 수 있습니다 이 또한 쌓이게 되죠 바이너리에서 최대 2% 코드 사이즈 개선이 있었습니다 메세지 전송 스텁과는 달리 런타임 지원이 필요한데요 iOS 16 tvOS 16 또는 watchOS 9와 같은 배포 대상으로 마이그레이션할 때 자동으로 받게 됩니다 이제 변경된 사항을 살펴봅시다 다시 예시로 돌아가 보죠 우리는 msgSend 호출에 대해 이야기했습니다 하지만 자동 참조 카운팅 또는 ARC를 사용하면 컴파일러에 의해 삽입된 retain/release 호출이 많아지는 건 똑같습니다 매우 높은 수준에서 객체에 포인터의 복사본을 만들 때마다 이를 살아있도록 보유 카운트를 증가시켜야 하죠 여기서는 변수 cal, dateComponent 및 theDate에서 발생합니다 objc_retain을 사용해 런타임을 호출해 이를 수행하죠 변수가 범위를 벗어나면 objc_release를 사용해 보유 카운트를 줄여야합니다 ARC 사용의 이점은 컴파일러 마법입니다 호출의 최소화를 위해 많은 호출을 제거해주죠 잠시 후 이런 마법 중 하나를 볼건데요 모든 마법에도 불구하고 다음의 호출은 여전히 필요합니다 여기 예시에서는 결국 cal과 dateComponents에 대한 로컬 복사본을 릴리스해야 합니다
내부적으로 이런 objc_retain/release 함수는 단순한 C 함수입니다 릴리스될 객체인 단일 인자를 취하죠 ARC를 사용하면 컴파일러가 이런 C 함수에 호출을 삽입해 적절한 객체 포인터를 전달합니다 그 때문에 이러한 호출은 애플리케이션 바이너리 인터페이스 또는 ABI에서 정의한 C 호출 관례를 준수해야하죠 호출을 수행하기 위해 더 많은 코드가 필요함을 의미합니다 올바른 레지스터에 포인터를 전달하기 위해서요 따라서 몇 가지 더 추가적인 '이동' 명령을 얻고 마는 것이죠 여기가 새로운 최적화가 도입되는 시점입니다 사용자 정의 호출 관례로 retain/release를 특수화하면 객체 포인터가 이미 있는 위치에 따라서 적절한 변형을 기회적으로 사용할 수 있습니다 따라서 이동이 필요 없도록 하죠 많은 중복 코드를 제거할 수 있음을 의미합니다 모든 호출에 대해서요 이런 사소한 명령에서는 별 것 아닌 것처럼 보일 수 있지만 전체 앱에서는 합쳐서 쌓이게 되죠 따라서 retain/relase 작업 비용을 더 저렴하게 만들게 된 것이죠 마지막으로 자동 릴리스 제거에 관해 이야기해보죠 훨씬 더 흥미롭습니다 objc 런타임 변경으로 자동 릴리스 제거를 더 빠르게 만들었습니다 이는 새 OS 릴리즈에서 기존 앱을 실행할 때 자동으로 발생하게 됩니다 더불어 컴퍼일러를 추가로 변경해 코드 또한 더 작게 만들었습니다 이 사이즈의 이점 또한 iOS 16, tvOS 16 또는 watchOS 9와 같은 배포 대상으로 마이그레이션 시 자동 제공됩니다
이것들 모두 훌륭합니다만 자동 릴리스 제거란 무엇일까요 다시 예시로 돌아가 보죠 앞서 ARC는 보유 및 릴리스 최적화를 위한 많은 컴파일러 마법을 제공한다고 말씀드렸는데요 여기서는 자동 릴리스된 반환 값 이 한 가지에만 집중해보겠습니다 이 예시에서 우리는 임시 객체를 생성해서 호출자에 반환합니다 작동 방식을 살펴보죠 임시 객체 theDate가 있습니다 이를 반환하고 호출이 완료됩니다 호출자는 이것을 자체 변수에 저장하죠 이것이 ARC와는 어떻게 작동하는지 봅시다 ARC는 호출자에 유지를 삽입하고 호출된 함수에 릴리스를 삽입합니다 여기에서 임시 객체를 반환할 때 함수에서 먼저 릴리스 해야합니다 범위를 벗어나기 때문이죠 하지만 아직은 그럴 수 없습니다 다른 참조가 아직 없기 때문이에요 이것을 먼저 릴리스 하게 되면 반환하기도 전에 파괴됩니다 그걸 원치 않죠 따라서 임시 객체를 반환할 수 있도록 특수 관례가 사용됩니다 반환하기 전 자동으로 릴리스해서 호출자가 유지할 수 있는 것이죠 이전에 자동 릴리스 및 자동 릴리스 풀을 본 적이 있을 겁니다 이는 단순히 릴리스 시점을 나중으로 연기하는 방법이죠 런타임은 릴리스 발생 시점에 대한 어떤 보장도 하지 않습니다 지금 바로가 아닌 한 편리한 것이죠 임시 객체 반환이 가능하도록 해주니까요 하지만 이건 공짜가 아닙니다 자동 릴리스를 수행하는 데는 약간의 오버헤드가 있죠 이때 자동 릴리스 제거가 사용됩니다 작동 방식 이해를 위해 어셈블리를 살펴보고 반환 값을 추적해 봅시다 autorelease를 호출하면 objc 런타임으로 이동하고 여기서부터 재미있어집니다 런타임은 무슨 일이 일어나고 있는지 인식하려고 합니다 자동 릴리스된 값을 반환을 말이죠 이를 돕기 위해 컴파일러는 이런 경우가 아니면 달리 사용하지 않는 특수 마커를 방출합니다 이는 런타임에 자동 릴리스 제거에 자격이 됨을 알리기 위함이죠 그리고 그 뒤에는 나중에 실행할 유지가 옵니다 하지만 지금은 아직 autorelease 안에 있으며 실행 시 런타임은 특수 마커 명령을 데이터로 로드합니다 이를 비교하면서 예상하는 특수 마커 값인지 확인하죠 예상된 값이라면 컴파일러가 런타임에 즉시 유지될 임시 객체를 반환한다고 알렸다는 의미입니다 일치하는 자동 릴리스 및 유지 호출을 생략 또는 제거 가능하게 하죠 이것이 자동 릴리스 제거입니다
하지만 이 또한 공짜가 아닙니다 코드를 데이터로 로드하는 것은 아주 일반적인 것이 아닙니다 CPU에 최적이 아니죠 우리는 더 잘할 수 있어요 반환 시퀀스를 다시 추적해봅시다 이번엔 새로운 방법을 사용해서요 우리는 autorelease에서 시작했습니다 이는 여전히 Objective-C 런타임으로 이어지죠 이 시점에서 우리는 이미 귀중한 정보 반환 주소를 가지고 있습니다 이 함수 실행이 완료되면 돌아가야하는 위치를 알려주죠 따라서 이것을 추적할 수 있습니다 고맙게도 반환 주소를 얻는 비용은 매우 저렴합니다 단지 포인터이기 때문에 옆에 저장해둘 수 있죠 그런 다음 autorelease 호출 런타임을 빠져나옵니다 호출자에게 돌아가고 유지를 수행할 때 런타임에 다시 들어가죠 이때 새로운 마법이 발생합니다 이 시점에서 우리가 있는 곳을 조회한 다음 현재 반환 주소로 포인터를 가져옵니다 유지를 수행하며 우리가 이전에 저장한 포인터와 방금 이 포인터를 런타임에서 비교하면서 자동 릴리스를 수행하는것이죠 이는 단지 두 개의 포인터를 비교하는 것이기 때문에 비용이 매우 저렴합니다 값비싼 메모리 액세스를 수행할 필요가 없죠 비교가 성공하면 자동 release/retain 쌍을 생략할 수가 있죠 약간의 성능 향상도 있습니다
게다가 이제 더 이상 이 특수 마커 명령어를 데이터로 비교할 필요가 없으므로 제거할 수 있고 그러면 코드 사이즈도 절약할 수 있습니다 우리가 자동 릴리스 제거를 더 빠르고 작게 만든 방법입니다 여러 Swift 및 Objective-C 런타임 개선 사항을 거쳤죠 이제 마무리해봅시다 앱이 새 OS에서 실행될 때는 런타임의 개선 덕분에 Swift 프로토콜 검사가 더 효율적입니다 매번의 자동 릴리스 제거 시도에도 더 빠르죠 Xcode 14의 새로운 컴파일러 및 링거와 메세지 전송 스텁은 앱을 다시 빌드하여 코드 사이즈를 최대 2%까지 절약해줍니다 마지막으로 배포 대상을 iOS 16, tvOS 16 또는 watchOS 9로 업데이트하면 2%를 추가로 절약할 수 있습니다 retain/release 호출을 더 작게 만들어서요 보다 작은 자동 릴리스 제거 시퀀스 덕에 더욱 그렇습니다 지금까지 Swift 및 Objective-C 런타임에 대한 심층 분석이 즐거웠기를 바라며 시청해주셔서 감사합니다
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.