View in English

  • 메뉴 열기 메뉴 닫기
  • Apple Developer
검색
검색 닫기
  • Apple Developer
  • 뉴스
  • 둘러보기
  • 디자인
  • 개발
  • 배포
  • 지원
  • 계정
페이지에서만 검색

빠른 링크

5 빠른 링크

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 주제
  • 전체 비디오
  • 소개

더 많은 비디오

  • 소개
  • 요약
  • 자막 전문
  • 코드
  • Swift로 메모리 사용량 및 성능 개선하기

    Swift 코드의 성능과 메모리 관리를 개선할 수 있는 방법을 알아보세요. 높은 수준의 알고리즘 변경을 수행하고 메모리 및 할당을 보다 세밀하게 제어할 수 있도록 새로운 InlineArray 및 Span 유형을 채택하는 등 코드를 개선하는 방법을 살펴보겠습니다.

    챕터

    • 0:00 - 서론 및 어젠다
    • 1:19 - QOI 형식 및 파서 앱
    • 2:25 - 알고리즘
    • 8:17 - 할당
    • 16:30 - 독점성
    • 19:12 - 스택 및 힙 비교
    • 21:08 - 레퍼런스 카운팅
    • 29:52 - Swift Binary Parsing 라이브러리
    • 31:03 - 다음 단계

    리소스

    • Performance and metrics
    • Swift Binary Parsing
    • The Swift Programming Language
    • The Swift website
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC25

    • 앱의 전력 사용량 프로파일링 및 최적화하기
    • Instruments를 사용하여 CPU 성능 최적화하기
    • Swift의 새로운 기능

    WWDC24

    • Swift의 성능 살펴보기
  • 비디오 검색…

    안녕하세요 Swift 표준 라이브러리에서 일하고 있는 Nate Cook입니다 오늘은 코드의 기능을 이해하고 개선하는 방법을 살펴보겠습니다 부분적으로는 Swift 6.2 언어와 표준 라이브러리에서 몇 가지 새로운 추가 기능을 사용함으로써 활용할 수 있습니다 새 InlineArray 및 Span 유형을 사용하고, 값 제네릭을 써보고 비탈출 유형에 대해 알아볼 것입니다 모든 이러한 새 도구를 사용해 유지와 릴리스, 독점성과 고유성 검사, 기타 추가 작업도 생략할 것입니다 또한 이 모든 도구의 새 오픈 소스 라이브러리도 소개하겠습니다 바이너리 파서를 빠르고 안전하게 작성할 수 있게 해주죠 이것을 Swift 바이너리 파싱이라고 합니다 라이브러리는 속도를 초점을 맞추고 관리 도구를 제공해 여러 가지 종류의 안전 장치를 제공합니다 모두가 빠른 코드를 원하죠 Swift가 도구를 제공합니다 하지만 때로는 기대하는 만큼 일이 빠르게 진행되지 않는 경우도 있죠 이 세션에서는 코드의 시간 사용처를 파악하는 연습을 해 보죠 그리고 여러 종류의 성능 최적화를 시도해 볼 것입니다 올바른 알고리즘 선택하거나 불필요한 할당을 제거하고 독점성 검사를 제거하거나 힙에서 스택 할당으로 이동하고 그리고 참조 계산을 줄이는 등의 작업입니다 이번 영상에서는 제가 만든 작은 앱을 살펴보겠습니다 QOI라는 이미지 형식을 위한 뷰어입니다 여기에는 형식에 대한 손으로 쓴 파서가 포함되어 있습니다 QOI는 사양이 적합할 만큼 간단한 손실 없는 이미지 형식입니다 단일 페이지에서 여러 접근 방식을 시도하고 성능을 확인하는 데 유용합니다 QOI 형식은 고정 크기 헤더와 데이터 섹션을 사용해 이진 형식에 대한 고정 크기 헤더 표준 관용구를 사용합니다 여기에는 다양한 크기의 인코딩된 픽셀이 동적으로 포함되죠 인코딩된 픽셀은 여러 가지 형태를 띱니다 픽셀은 RGB 또는 RGBA 값일 수 있으며, 이전 픽셀과의 차이점이죠 이전에 본 픽셀의 캐시를 조회하는 것이나 이전 픽셀을 반복하는 횟수일 수도 있습니다 좋습니다 제 QOI 파서 앱을 사용해 보죠

    몇 킬로바이트에 불과한 이 아이콘 파일을 열어보겠습니다 즉시 로딩되죠

    이 새 사진은 조금 더 큽니다 로딩하는 데 몇 초 정도 걸릴 수 있습니다 그러면 이렇게 됩니다 왜 이렇게 오래 걸리는 걸까요? 실제 데이터로 작업할 때 현저하게 속도가 저하되는 경우 이는 종종 알고리즘이나 데이터 구조를 잘못 사용한 경우에 해당합니다 Instruments를 사용하여 이 문제의 근원을 찾아 해결해 보겠습니다 내 파싱 라이브러리에서 동일한 새 이미지를 파싱하는 테스트를 작성했는데 로딩 속도가 느렸죠 테스트를 실행하기 위해 실행 버튼을 클릭해보겠습니다

    몇 초 후에 패스되죠 이 테스트를 사용하여 정확성을 확인하는 것 외에도 테스트 프로파일링으로 성능도 Instruments에서 볼 수 있죠 이번에는 실행 버튼의 보조 클릭을 사용합니다 메뉴에 테스트를 프로파일링하는 옵션이 있네요

    저는 이 기능을 좋아해요 테스트를 프로파일링할 때 코드의 특정 부분에 집중하도록 해주죠 내가 관심 있는 부분에만요 이제 해당 옵션을 선택해서 Instrument를 실행하겠습니다

    Instruments는 템플릿 선택기로 시작되며 다양한 방법을 보여주죠 이는 코드의 성능을 이해하는 데 도움이 될 수 있습니다 오늘은 두 가지 다른 Instruments를 사용할 거예요 그럼 빈 템플릿으로 시작하겠습니다

    Instruments 추가 버튼을 클릭하면 Instruments를 추가할 수 있습니다 이해를 돕기 위해 Allocations 도구를 추가하겠습니다. 파서가 메모리를 어떻게 할당하는지 이해하기 위해서요 그리고 저는 앱이 어디에 시간을 쓰는지 정말 알고 싶으니 “시간 프로파일러” 도구를 추가하겠습니다

    시간 프로파일러는 성능에 대한 질문을 해결하기에 좋은 곳입니다 결과 공간을 넓히기 위해 사이드바를 숨겨보겠습니다 그런 다음 녹음 버튼을 사용하여 테스트를 시작하세요

    결과 창에서 몇 가지 사항을 확인할 수 있습니다

    프로필에 포함된 Instruments는 창 상단에 나열됩니다 먼저 “시간 프로파일러”를 사용 할 테니 선택된 상태로 두겠습니다 하단에는 선택한 도구에 대한 세부정보 보기가 있습니다 왼쪽에는 캡처된 통화 목록이 있습니다 오른쪽은 현재 선택된 호출에 대한 가장 무거운 스택 추적이 있습니다

    가장 자주 캡처된 통화를 먼저 보려고 합니다 그들이 어떻게 도달하든 상관없이 말이죠 그러니 호출 트리 버튼을 클릭할게요

    그런 다음 “호출 트리 반전” 확인란을 선택합니다 이 버튼을 사용하여 그래픽 보기로 전환할 수 있습니다 세부 사항 보기 상단에 있죠 클릭하면 프로필을 화염 그래프로 표시하도록 보기가 전환됩니다

    화염 그래프의 각 막대는 비율을 보여줍니다 프로필 작성 중에 호출이 포착된 비율입니다 이 경우에는 프로세스를 지배하는 거대한 막대가 있습니다 “platform_memmove”로 라벨이 지정되었죠 동일한 기호가 스택 추적에 나타나고 memmove는 데이터 복사용 시스템 호출이라 큰 막대가 나오고 파서가 데이터를 읽기만 하지 않고 복사를 하는 데 가장 많은 시간을 소비하고 있다는 것을 나타내죠 이래서는 안되죠 코드의 어느 부분 때문에 복사가 발생하는지 알아봅시다 스택 추적의 프레임에서 모든 것을 볼 수 있으면 좋겠어요 보기 상단의 “모든 프레임 표시” 버튼을 클릭합니다

    추적 맨 위에 platform_memmove를 포함한 시스템 호출이 있습니다 파운데이션 데이터 유형에 의해 제공된 몇 가지 전문화된 메서드 버전도 있죠 전문적인 메서드를 스택 추적이나 디버깅할 때 보셨을 수도 있습니다 이러한 전문적인 메서드는 Swift 컴파일러가 생성해 준 일반 코드의 유형별 버전입니다

    마지막으로 매서드에 도달했습니다 제가 정의한 것은 readByte입니다

    이것이 내 코드에서 문제와 가장 가까운 함수이니 여기서부터 시작하는 게 좋겠습니다 바로 이 메서드를 사용하고자 보조 클릭을 사용하고 “Xcode에서 보기”를 선택하죠

    그리고 Xcode의 readByte 메서드에 대한 선언문이 있습니다 Instruments가 이 라인에 보내네요 여기에 첫째 바이트를 드롭한 후 데이터 이니셜라이저를 호출합니다. Instruments를 사용해 그 모든 memmove 호출이 라이브러리가 느려진 잠재적 원인으로 식별하고 그 다음 복사 작업의 원인인 코드로 바로 이동할 수 있었습니다

    이 도우미 메서드는 정말 중요합니다 파싱 코드가 원시 바이너리 데이터를 소비하면서 readByte를 계속해서 호출하기 때문이죠

    이렇게 하면 그냥 데이터가 줄어들고 첫 번째 바이트를 반환하고 데이터 시작을 앞으로 옮길 줄 알았죠 readByte 메서드를 호출할 때마다 말이죠 그 대신 실제로는 바이트를 읽을 때마다 데이터 전체 내용을 새로 할당해 복사하고 있습니다 예상했던 것보다 훨씬 더 많은 작업이네요 이 실수를 바로 잡아 보죠 이제 Xcode로 돌아와 readByte 메서드를 편집합니다

    데이터 유형은 양쪽 끝에서 수축되도록 설계되었기 때문에 ‘popFirst()’라는 컬렉션 메서드에 직접 접근할 수 있죠 popFirst()는 ‘데이터’의 첫 바이트를 반환합니다 그후 컬렉션의 앞 부분을 앞으로 밀어내고 한 바이트 줄입니다 바로 원하는대로에요

    이렇게 고치면 테스트로 돌아가 프로필을 다시 실행할 수 있어요

    테스트가 먼저 실행된 상태에서 Instruments가 자동으로 열립니다 동일한 프로파일링 구성을 사용하죠 훌륭합니다 거대한 플랫폼_memmove 막대가 화염 그래프에서 사라졌습니다

    코드를 벤치마킹하면 속도가 매우 빨라지는 것을 볼 수 있어요 변경 사항 덕분이죠 너무 좋지만, 이와 같은 알고리즘 변경으로 인한 절대적인 변화가 다는 아닙니다 원래 버전에서는 이미지 크기와 파싱에 소요된 시간 간의 관계는 2차 방정식과 같습니다 파싱하는 이미지가 커지면서 파싱에 걸리는 시간이 엄청나게 길어졌습니다 복사 문제가 해결되면서 관계는 이제 선형이 되었습니다. 이미지 크기와 파싱 소요 시간 사이에는 더 직접적인 연관성이 있습니다 선형 성능을 개선시킬 개선 사항이 많이 예정되어 있습니다 개선 사항을 더 직접적으로 비교할 수 있게 될 것입니다

    문제가 해결되었으니 일반적인 성능 함정 중 하나인 추가 할당에 대해 살펴보겠습니다 지금 가장 무거운 스택 추적이 무엇인지 살펴보겠습니다 이러한 호출은 메서드에 많은 트래픽이 있음을 보여줍니다 Swift 배열을 할당하고 해제하는 배열 말이죠 메모리를 할당하고 해제하는 일은 비용이 많이 들 수 있습니다 추가적인 할당이 어디서 비롯되었는지 알아내고 지울 수 있다면 파서가 더 빨라질 것입니다 내 파서가 수행하는 할당을 보려면 이전에 추가한 할당 도구를 사용할 수 있습니다 코드가 불필요한 할당을 초래하고 있다는 몇 가지 다른 지표가 있습니다 첫 번째는 엄청난 숫자입니다 하나의 이미지를 파싱하는 데 백만 개의 할당이 필요할까요? 더 좋은 방법이 있겠죠 둘째, 거의 모든 할당이 일시적 할당임을 알 수 있습니다 할당 도구에 의해 단기로 표시된 것이죠

    문제의 근원을 찾으려면 세부 정보 패널을 호출 트리 뷰로 전환합니다 먼저, 통계라고 표시된 팝업 버튼을 클릭합니다 그리고 콜 트리를 선택하겠습니다

    상단 스레드를 선택한 상태에서 스택 추적을 살펴보고 해당 부분을 찾아볼게요 문제에 가장 가까운 코드가 있는 부분을요 이 스택 추적은 반전되지 않으므로 추적의 맨 아래부터 살펴봐야겠어요 파서의 첫 번째 심볼은 이 RGBAPixel.data 메서드입니다

    해당 메서드를 클릭하면 호출 트리 세부 정보 창이 표시되죠 그리고 그 메서드에 보조 클릭을 사용하면 Xcode에서 Reveal을 선택하고 소스로 바로 이동할 수 있습니다

    이 메서드가 추가 할당의 원인인 것 같습니다 호출될 때마다 픽셀의 RGB 또는 RGBA 값을 포함하는 배열을 반환하는 것을 볼 수 있습니다 즉, 배열을 생성하고 공간을 할당한다는 의미입니다 호출될 때마다 최소 3개의 요소에 대해서요

    어디에서 사용되는지 알아보려면 함수 이름에 보조 클릭을 사용하고 “발신자 표시”를 선택합니다

    호출자는 주요 파싱 함수에 있는 이 클로저입니다 이는 큰 flatMap과 접두사 체인의 일부일 뿐이죠 이 코드가 왜 이렇게 할당을 많이 하는지 이해하기 위해 할당이 어떻게 단계별로 쌓이는지 살펴보겠습니다

    먼저, readEncodedPixels 메서드는 바이너리 데이터를 코딩된 픽셀로 파싱하죠 제가 앞서 언급한 다양한 픽셀 유형입니다 이를 저장하기 위해서는 충분한 공간을 할당해야 합니다

    다음으로, 인코딩된 각 픽셀에 대해 decodePixels가 호출됩니다 하나 이상의 RGBA 픽셀을 생성하기 위해서죠 대부분의 인코딩은 단일 픽셀로 변환되지만 이전 픽셀을 특정 횟수만큼 반복하라는 인코딩이 있습니다 이를 지원하려고 decodePixels는 항상 배열을 반환합니다 각 배열에는 할당이 필요합니다

    flatMap의 “평평화” 부분은 방금 생성한 모든 작은 배열을 가져와 병합해, 훨씬 더 큰 하나의 배열로 만듭니다 새로운 할당이죠 그러면 방금 만든 모든 작은 배열이 할당 해제됩니다

    이 접두사 메서드는 생성할 수 있는 픽셀 수에 제한을 둡니다

    두 번째 flatMap은 RGBAPixel.data를 호출합니다 할당 도구를 사용할 때 플래그를 지정한 메서드죠 이미 전에 3개 또는 4개 요소로 이루어진 배열을 반환한다는 것을 확인했죠 지금 우리가 보고 있는 것도 최종 이미지의 각 픽셀에 대해 3개 또는 4개 요소 배열 중 하나가 생성되고 있는 것이죠 때때로 컴파일러는 이 추가 할당 중 일부를 최적화할 수 있습니다 하지만 추적에서 보았듯 항상 일어나는 일은 아닙니다

    다음으로, 작은 배열이 다시 하나의 큰 새 배열로 평면화됩니다 마지막으로 RGB 또는 RGBA 픽셀 데이터의 큰 배열이 새 Data 인스턴스에 복사되어 반환될 수 있게 됩니다

    이 코드 줄에서 우아함이 느껴집니다 몇 개의 짧고 연결된 메서드 호출이 아주 강력합니다 하지만 더 짧다고 해서 더 빠른 것은 아닙니다 그 모든 다른 단계를 거쳐 작업을 진행하고 종전에 반환할 Data 인스턴스와 함께 끝나는 대신 데이터를 먼저 할당하고 각 픽셀을 쓰는 것처럼 이진 소스 데이터에서 디코딩한다면? 그렇게 하면 중간 할당 없이도 모두 동일한 처리를 할 수 있습니다 다시 파싱 기능으로 돌아오겠습니다 이 메서드를 다시 써서 모든 추가 할당을 제거해봅시다

    가장 먼저 할 일은 “totalBytes” 계산입니다 결과 데이터에 대한 최종 크기 말이죠 이후 “pixelData”를 할당합니다 딱 적당한 양의 저장 공간으로요 “오프셋” 변수는 계속해서 데이터 사용량을 추적합니다 이 사전 할당은 추가 할당을 할 필요가 없음을 의미합니다 이진 데이터를 처리하는 와중에 말이죠

    다음으로, 각 데이터를 파싱하여 즉시 처리할 것입니다 switch 문을 사용해 파싱된 픽셀을 처리합니다

    실행을 나타내는 인코딩된 픽셀의 경우 필요한 횟수만큼 반복해 매번 픽셀 데이터를 작성합니다

    다른 종류의 픽셀의 경우 디코딩 하여 데이터에 직접 적습니다 반환이 필요한 데이터를 제외하면 할당 없이 전부 재작성하는 것이죠 이제 테스트 프로파일링을 통해 이 문제를 해결했는지 알아보죠

    할당된 숫자가 훨씬 적은 것을 바로 확인할 수 있습니다 코드에서 실제 할당 수를 보려면 필터를 사용할 수 있습니다 창의 아래쪽의 필터 필드를 클릭하겠습니다 그리고 “QOI.init”를 입력합니다

    모든 호출 트리를 필터링하도록요 스택 추적 어딘가에 QOI.init이 포함되지 않게 됩니다 나머지 줄은 파서 코드가 소수의 할당을 실행해 총량이 총 2메가바이트에 약간 못 미치는 것을 볼 수 있습니다 옵션을 누른 채로 공개 삼각형을 클릭하면 호출 트리가 확장됩니다

    확장된 트리는 우리가 원하는 것을 보여줍니다

    실제로 할당하는 유일한 것은 결과 이미지의 저장 데이터입니다

    벤치마크를 살펴보면 정말 큰 개선이죠 이러한 추가 할당을 잘라내면 실행 시간이 절반 이상 단축되었습니다

    지금까지 파서에 두 가지 알고리즘을 만들었습니다 원치 않는 복제를 제거하고 총 할당수를 줄이는 파서 변경 사항이죠 다음 몇 가지 개선 사항을 위해 좀 더 진보된 기술을 사용해 Swift 컴파일러를 허용하여 많은 자동 메모리 관리 작업 기능을 제거하겠습니다 런타임 중 일어나는 일이죠

    먼저 배열과 다른 컬렉션 유형에 대해 나눠보겠습니다 Swift의 Array 유형은 도구 상자에서 가장 흔한 도구죠 빠르고, 안전하고 사용하기 쉽기 때문이죠 배열은 필요에 따라 확장되거나 축소될 수 있습니다 따라서 작업할 품목의 수를 미리 알 필요가 없죠 Swift는 백그라운드에서 메모리를 처리합니다 배열도 값 유형이므로 한 배열의 복제본에 대한 변경 사항은 다른 사본에 영향을 미치지 않습니다 배열의 복사본을 만드는 경우 다른 변수에 할당하거나 또는 함수에 전달하는 방식을 취하면 Swift는 요소를 즉시 복제하지 않습니다 대신, 복사-쓰기라는 최적화를 사용합니다 배열 중 하나를 실제로 변경할 때까지 복제를 지연하죠

    이러한 기능들은 배열을 훌륭한 범용 컬렉션으로 만듭니다 하지만 이 역시 장단점이 있죠 동적 크기와 여러 참조를 지원하기 위해 배열은 종종 힙에 별도로 할당하여 내용을 저장합니다 Swift 런타임은 참조 계산을 사용해 각 배열의 사본 수를 추적합니다 변경 사항을 적용하면 배열은 고유성 검사를 수행하여 요소를 복사해야 하는지 확인하죠 마지막으로 코드를 안전하게 유지하기 위해 Swift는 배타성을 강화합니다 즉, 두 가지 다른 요소는 동시에 같은 데이터를 수정할 수 없죠 이 규칙은 종종 컴파일 시간에 적용되지만 때로는 런타임에만 적용될 수도 있습니다 이제 낮은 수준의 개념에 대해 배웠습니다 프로파일링에서 이것이 어떻게 나타나는지 살펴보겠습니다 독점성을 위한 런타임 검사를 찾는 것부터 시작하겠습니다 이는 프로그램에 작업을 추가할 수 있고 최적화를 방해할 수 있죠 독점성 확인을 시작하기 전에 사실은 좋은 문제를 살펴보겠습니다 퍼포먼스 향상이 충분히 이루어져 Instruments가 파서 프로세스를 검사할 시간이 부족하다는 거죠 파싱 코드를 반복하여 좀 더 자세히 살펴보겠습니다 50번 정도면 충분할 거예요

    더 풍부한 프로필을 살펴보겠습니다

    독점성 테스트는 추적에서 ‘swift_beginAccess’로 표시되죠 그리고 ‘swiftendAccess’ 심볼로도 표시됩니다 다시 한번 창 하단의 필터 상자를 클릭합니다 그런 다음 기호 이름을 입력합니다

    화염 그래프 상단에는 swift_beginAccess가 몇 번 나오죠 바로 아래에 이러한 확인이 필요한 기호와 함께요 이러한 심볼은 이전 픽셀과 픽셀 캐시에 대한 접근자입니다 이는 파서의 State 클래스에 저장됩니다 Xcode로 돌아가서 해당 선언을 찾아보겠습니다 바로 이렇게요 State는 화염 그래프에서 본 두 가지 속성을 갖춘 클래스입니다 클래스 인스턴스를 수정하는 것은 다음 상황 중 하나에 해당합니다 Swift가 런타임에 배타성을 확인해야 하는 경우죠 이를 살펴보는 이유가 바로 이 선언입니다 이러한 속성을 클래스 밖으로 이동시켜 그 확인을 제거할 수 있습니다 그리고 이를 파서 유형에 직접 넣죠

    다음으로 찾기-바꾸기를 수행하겠습니다 previousPixel과 pixelCache로의 ‘state’ 접근을 제거하려고요

    빌드할 때 컴파일러는 약간의 작업이 더 필요하다고 알려줍니다

    상태 속성이 더 이상 클래스에 중첩되지 않으므로 변형할 수 없는 메서드에서는 수정할 수 없습니다

    이 수정 사항을 수용하면 메서드가 변형되게 합니다

    고칠 것이 하나 더 있습니다

    이게 다입니다 변경 사항을 적용한 후 테스트로 돌아가 보겠습니다

    프로필을 다시 기록하면 변경 사항을 확인할 수 있습니다

    swift_beginAccess를 다시 필터링해보겠습니다

    이제 아무것도 없네요 런타임 독점성 검사를 완전히 제거했습니다 다시 한번 상태 변수를 살펴보겠습니다 새로운 Swift 기능을 사용하기에 좋은 곳입니다 힙 메모리에서 스택 메모리로 데이터를 이동하고 독점성 검사가 다시 끼어들지 못하도록 할 수 있죠 파서의 픽셀 캐시는 RGBAPixels 배열입니다 64개의 요소로 초기화되며 크기가 변경되지 않죠 이 캐시는 새로운 InlineArray 유형을 사용하기에 좋습니다 InlineArray는 Swift 6.2의 새로운 표준 라이브러리 유형입니다 일반 배열과 마찬가지로 연속 메모리에 같은 유형의 여러 요소를 저장하죠 하지만 몇 가지 중요한 차이점이 있습니다 첫째, 인라인 배열은 컴파일 시에 설정된대로 고정된 크기를 갖죠 추가하거나 제거할 수 있는 일반 배열과 달리 InlineArray는 새로운 값 제네릭 기능을 사용합니다 크기를 유형의 일부로 만들기 위해서죠 즉, 인라인 배열의 요소를 변경할 수 있지만 다른 크기의 인라인 배열을 추가 하거나 제거하거나 할당할 수 없죠

    둘째, 이름에서 알 수 있듯이 InlineArray를 사용하면 요소는 별도의 할당이 아닌 항상 인라인으로 저장됩니다 인라인 배열은 복사본 간에 저장소를 공유하지 않습니다 복사-쓰기 기능을 사용하지 않죠 대신, 사본을 만들 때마다 해당 내용이 복사됩니다 이렇게 하면 참조 계산과 고유성 등 모든 것이 필요 없게 됩니다 일반 배열에 필요한 독점성 검사도 마찬가지죠 InlineArray의 남다른 복사 동작은 양날의 검과도 같습니다 배열을 사용하는 데 여러 변수 또는 클래스 간에 사본 만들기나 참조 공유할 때 InlineArray는 나쁜 선택이죠 그러나 이 경우에는 픽셀 캐시는 고정 크기 배열입니다 그 자리에서 수정되었지만 결코 복사되지는 않죠 ‘InlineArray’를 사용하기에 완벽한 곳입니다

    최종 최적화를 위해 표준 라이브러리의 새로운 스팬 유형을 사용할 것입니다 파싱 중에 대부분의 참조수를 제거하기 위해서죠 시간 프로파일러 화염 그래프로 돌아가서 다시 필터링을 사용해 QOI 파서만 살펴 보겠습니다 필터 상자에 QOI.init을 추가하겠습니다

    보기가 스택 추적에만 집중하도록 변경됩니다 여기에는 파싱 초기화 프로그램이 포함되죠 보존 및 해제 심볼을 찾습니다 swift_retain은 분홍 막대입니다 샘플의 7%에 나타나죠 swift_release는 이것이고 또 다른 7%에 나타납니다 앞서 이야기한 고유성 검사도 이곳에 나타납니다 또 다른 샘플의 3%에서 말이죠

    이들이 어디서 오는지 알아내고자 swift_release를 클릭하겠습니다 이전처럼 가장 무거운 스택 추적을 스캔해 첫째 사용자 정의 메서드를 찾죠 시작했던 방법과 같은 readByte 메서드 같군요

    이번에는 우리가 다루고 있는 알고리즘 문제가 아닙니다 ‘데이터’ 사용 그 자체에 있는 문제죠 ‘Array’처럼 ‘Data’도 보통 메모리를 힙에 저장합니다 참조 카운트가 필요하죠 유지 및 해제라는 참조 계산 작업은 매우 효율적입니다 하지만 루프가 타이트할 때는 시간이 많이 늘어날 수 있습니다 바로 이 방법과 같죠 이를 해결하기 위해 ‘데이터’ 또는 ‘배열’과 같은 높은 수준의 컬렉션 유형을 작업하는 것에서 이동해야 합니다 이러한 유형은 참조 계산이 폭발적으로 늘어나지 않도록 하죠 Swift 6.2까지는 ‘withUnsafeBufferPointer’ 메서드 같은 방법으로 컬렉션의 기본 저장소에 액세스했습니다 이러한 메서드를 사용하면 메모리를 수동으로 관리할 수 있죠 참조 계산 없이도요, 하지만 코드에 비안전성이 추가되죠

    포인터가 안전하지 않은 이유는 무엇일까요? 이 요소는 여러 언어의 안전 보장을 벗어나기 때문에 Swift는 이를 안전하지 않은 것으로 봅니다 이는 초기화된 메모리와 그렇치 않은 메모리를 포인트하죠 일부 유형의 보증을 없애고 자신의 맥락에서 벗어날 수 있기 때문에 비할당 메모리에 액세스하는 접근 위험으로 이어질 수 있습니다 안전하지 않은 포인터를 사용할 때에 코드의 안전성을 온전히 유지하는 것은 전적으로 여러분의 책임이죠 컴파일러는 도움을 줄 수 없습니다 이 processUsingBuffer 함수는 비안전 포인터를 올바르게 사용하죠 사용법은 전적으로 안전하지 않은 버퍼 포인터 클로저 내에 있습니다 계산의 결과만 마지막에 반환됩니다 반면에 이 ‘getPointerToBytes()’ 함수는 위험합니다 여기에는 두 가지 주요 프로그래밍 오류가 포함되어 있습니다 이 함수는 바이트 배열을 생성하고 UnsafeBufferPointer 메서드를 호출합니다 그러나 클로저에 대한 포인터의 사용을 제한하는 대신 이는 외부 범위에 대한 포인터를 반환합니다 오류 번호 1 더 나쁜 것은 코드가 함수 자체에서 더 이상 유효하지 않은 포인터를 반환한다는 점입니다 오류 번호 2 이 두 오류 모두 포인터의 수명을 포인트하는 대상의 수명을 넘어서도록 연장합니다 이동되거나 할당 해제된 메모리에 대한 위험한 잔여 참조를 생성하죠 이를 돕기 위해 Swift 6.2에서는 Span이라는 새로운 유형 그룹을 도입했습니다 스팬은 컬렉션에 속한 연속 메모리에 대해 작업하는 새로운 작업 방식입니다 중요한 점은 span이 새 “비탈출” 언어 기능을 사용한다는 것입니다 컴파일러가 이를 제공하는 컬렉션에 수명을 연결할 수 있도록 하죠 span이 액세스를 주는 메모리의 수명은 span의 수명만큼 보장되죠 지속적인 참조가 발생할 가능성이 없습니다 모든 span 유형이 비탈출형으로 선언되기 때문에 컴파일러가 탈출하거나 span을 검색한 컨텍스트 외부에서 span을 반환하지 못 하도록 합니다 이 “processUsingSpan” 메서드는 span을 사용해 포인터가 허용하는 것보다 더 간단하고 안전한 작성법을 보여줍니다 배열의 요소에 대한 Span은 span 속성을 사용하면 됩니다 클로저를 사용하지 않고도 배열의 저장소에 접근할 수 있죠 안전하지 않은 포인터만큼 효율적입니다 안전상의 문제가 전혀 없죠 비탈출 언어 기능이 작동하는 모습을 볼 수 있습니다 이전의 위험한 함수를 다시 작성하려고 하면 말이죠 직면하게 될 첫 번째 일은 ‘Span’으로는 같은 함수 시그니처 작성을 못하는 것입니다 span의 수명은 그것을 제공하는 컬렉션에 연결되어 있기 때문이죠 컬렉션이나 span이 전달되지 않은 상태에서는 전달되는 span의 수명을 받을 수 있는 곳은 어디에도 없습니다

    컴파일러로부터 span을 숨기고자 클로저로 span을 캡처하면요? 이 함수에서 배열을 생성하고 해당 span에 액세스하겠습니다 그런 다음 해당 span을 캡처하는 클로저를 반환해 보겠습니다 이것도 효과가 없네요 컴파일러는 span을 캡처하면 탈출할 수 있다는 것을 인식하고 그 수명이 로컬 배열에 따라 달라진다는 점을 짚어냅니다 이 컴파일러 검사 요건은 span이 범위를 벗어나지 않는 것입니다 즉, 유지 및 해제가 필요하지 않습니다 비안전 버퍼를 사용해도 안전 문제가 발생하지 않습니다 ‘Span’ 계열에는 읽기 전용 및 변경 가능한 span의 입력 버전과 원시 버전이 포함됩니다 기존 컬렉션을 사용해 작업하기 위한 것이죠 새로운 컬렉션 초기화에 사용할 수 있는 출력 span도 있습니다 이 계열에는 안전하고 효율적인 유니코드 처리를 위해 설계된 새로운 유형인 UTF8Span도 포함되어 있습니다

    코드로 돌아가서 RawSpan에 대해 동일 readByte 메서드를 구현하죠

    먼저 RawSpan 확장 프로그램을 추가해 시작하겠습니다

    그리고 readByte 메서드를 정의 하겠습니다

    RawSpan의 API는 Data와 약간 다르지만 같은 작업을 수행합니다 위의 구현과 같죠 첫 번째 바이트를 로드하고 RawSpan을 축소한 다음 로드된 값을 반환합니다 이 unsafeLoad 메서드는 이런 방식으로만 명명됩니다 특정 유형의 유형을 로드하는 것은 안전하지 않을 수 있기 때문입니다 여기서 하는 것처럼 내장된 정수 유형을 로드하면 항상 안전합니다

    다음으로, 파싱 방법을 업데이트하겠습니다

    이 두 가지 파싱 방법에서는 RawSpan을 사용해야 합니다 데이터를 매개변수로 사용하는 대신 말이죠

    호출 사이트에서도 변경이 필요합니다

    데이터 자체를 전달하는 대신 데이터의 RawSpan을 가져와서 파싱 메서드에 전달합니다 ‘bytes’ 속성을 사용하여 Data의 RawSpan에 접근하겠습니다 이 rawBytes 값은 탈출할 수 없습니다 이 함수에서 반환할 수 없습니다 하지만 아무런 문제 없이 파싱 메서드에 전달할 수 있죠

    이 변경으로 RawSpan을 사용하는 업데이트가 모두 완료되었습니다 더욱 낮은 수준의 작업을 저장하려면 파싱 방법에서 새로운 OutputSpan을 채택할 수 있습니다

    0으로 초기화된 데이터를 생성하는 대신 새로운 rawCapacity 초기화 프로그램을 사용할 것입니다 초기화되지 않은 데이터를 서서히 채우는 OutputSpan을 제공하죠

    OutputSpan은 작성한 데이터 양을 추적합니다 따라서 별도의 오프셋 변수 대신 count 속성을 사용할 수 있습니다

    그리고 쓰기 메서드의 변형 버전을 사용할 것입니다 데이터 인스턴스 대신 outputSpan에 쓰는 방식이죠

    메서드의 구현을 살펴봅시다

    write(to:) 메서드는 OutputSpan의 append 메서드를 호출합니다 픽셀의 각 채널에 대한 작업이죠 OutputSpan은 이런 용도로 설계된 비탈출 유형이므로 ‘Data’ 인스턴스에 쓰는 것보다 이것이 더 간단하고 효율적입니다 그리고 비안전 버퍼 포인터로 드롭하는 것보다 더 안전하죠 해당 변경 사항이 완료되었으니 테스트로 돌아가겠습니다 새로운 프로필을 기록하겠습니다

    QOI.init로 필터링하겠습니다

    이제 화염 그래프에서 swift_retain을 볼 수 있습니다 swift_release 블록이 사라졌죠 멋져 보이네요 여기서 멈추고 InlineArray와 RawSpan을 채택한 결과를 보죠

    몇 가지 변경 사항을 반영하니 메모리 관리 작업 덕분에 파싱 속도가 6배 빨라졌습니다 비안전 코드를 사용하지 않고도 말이죠 2차 알고리즘을 제거하니 예상했던 것보다 16배 더 빠릅니다 처음 시작했던 것에 비해서는 700배 이상 더 빠릅니다 이 세션에서는 많은 내용을 다루었습니다 이미지 파싱 라이브러리를 수정 하면서 두 개 알고리즘을 변경했죠 더 효율적으로 운영하고 할당을 줄이려는 목적이었습니다 InlineArray와 RawSpan이라는 새 표준 라이브러리 유형을 썼죠 런타임 메모리 관리를 생략하기 위해서였습니다 그리고 새로운 비탈출 언어적 특징을 배웠습니다 새 Swift 바이너리 파싱 라이브러리는 동일 기능을 기반으로 구축되었죠 라이브러리는 효율적인 바이너리 파서의 구축을 위해 설계됐습니다 그리고 여러 종류의 안전을 핸들링 하는 데 개발자를 지원합니다 라이브러리는 초기화 프로그램과 기타 도구 파싱 세트를 제공하죠 원시 이진 데이터의 값을 안전하게 소비할 수 있도록 안내합니다

    다음은 QOI 헤더에 대한 파서의 예입니다 새로운 라이브러리를 사용하여 작성되었죠 여기에는 ParserSpan을 포함한 여러 가지 특징이 표시됩니다 이진 데이터 파싱을 위한 맞춤형 원시 span 유형입니다 정수 오버플로를 방지하는 파싱 이니셜라이저죠 서명 여부, 비트 폭과 바이트 순서를 지정할 수 있습니다 라이브러리는 또한 커스텀 원시 표시 유형에 대한 파서 검증을 제공합니다 안전한 계산을 위해, 선택 생산 연산자에 대해서도 제공하죠 신뢰할 수 없는 새로 파싱된 값을 사용합니다

    바이너리 파싱 라이브러리는 Apple 내부에서 이미 사용 중이며 오늘부터 대중에게 공개됩니다 한번 살펴보고 시도해 보시기를 바랍니다 Swift 포럼에서 게시물을 게시하거나 문제를 제기하고 GitHub에서 풀 리퀘스트를 보내 커뮤니티에 참여할 수 있습니다 Swift 코드를 최적화하는 여정에 함께해 주셔서 정말 고맙습니다 Xcode와 Instruments을 사용해 테스트 프로파일링을 해보세요 앱의 성능에 중요한 부분에 활용해보세요 설명서에서 새로운 InlineArray 및 Span 유형을 탐색하거나 또는 Xcode의 새로운 버전을 다운로드하세요 멋진 WWDC가 되시길 바랍니다

    • 7:01 - Corrected Data.readByte() method

      import Foundation
      
      extension Data {
        /// Consume a single byte from the start of this data.
        mutating func readByte() -> UInt8? {
          guard !isEmpty else { return nil }
          return self.popFirst()
        }
      }
    • 9:56 - RGBAPixel.data(channels:) method

      extension RGBAPixel {
        /// Returns the RGB or RGBA values for this pixel, as specified
        /// by the given channels information.
        func data(channels: QOI.Channels) -> some Collection<UInt8> {
          switch channels {
          case .rgb:
            [r, g, b]
          case .rgba:
            [r, g, b, a]
          }
        }
      }
    • 10:21 - Original QOIParser.parseQOI(from:) method

      extension QOIParser {
        /// Parses an image from the given QOI data.
        func parseQOI(from input: inout Data) -> QOI? {
          guard let header = QOI.Header(parsing: &input) else { return nil }
          
          let pixels = readEncodedPixels(from: &input)
            .flatMap { decodePixels(from: $0) }
            .prefix(header.pixelCount)
            .flatMap { $0.data(channels: header.channels) }
      
          return QOI(header: header, data: Data(pixels))
        }
      }
    • 12:53 - Revised QOIParser.parseQOI(from:) method

      extension QOIParser {
        /// Parses an image from the given QOI data.
        func parseQOI(from input: inout Data) -> QOI? {
          guard let header = QOI.Header(parsing: &input) else { return nil }
          
          let totalBytes = header.pixelCount * Int(header.channels.rawValue)
          var pixelData = Data(repeating: 0, count: totalBytes)
          var offset = 0
          
          while offset < totalBytes {
            guard let nextPixel = parsePixel(from: &input) else { break }
            
            switch nextPixel {
            case .run(let count):
              for _ in 0..<count {
                state.previousPixel
                  .write(to: &pixelData, at: &offset, channels: header.channels)
              }
            default:
              decodeSinglePixel(from: nextPixel)
                .write(to: &pixelData, at: &offset, channels: header.channels)
            }
          }
          
          return QOI(header: header, data: pixelData)
        }
      }
    • 15:07 - Array behavior

      var array = [1, 2, 3]
      array.append(4)
      array.removeFirst()
      // array == [2, 3, 4]
      
      var copy = array
      copy[0] = 10      // copy happens on mutation
      // array == [2, 3, 4]
      // copy == [10, 3, 4]
    • 19:47 - InlineArray behavior (part 1)

      var array: InlineArray<3, Int> = [1, 2, 3]
      array[0] = 4
      // array == [4, 2, 3]
      
      // Can't append or remove elements
      array.append(4)
      // error: Value of type 'InlineArray<3, Int>' has no member 'append'
      
      // Can only assign to a same-sized inline array
      let bigger: InlineArray<6, Int> = array
      // error: Cannot assign value of type 'InlineArray<3, Int>' to type 'InlineArray<6, Int>'
    • 20:23 - InlineArray behavior (part 2)

      var array: InlineArray<3, Int> = [1, 2, 3]
      array[0] = 4
      // array == [4, 2, 3]
      
      var copy = array    // copy happens on assignment
      for i in copy.indices {
          copy[i] += 10
      }
      // array == [4, 2, 3]
      // copy == [14, 12, 13]
    • 23:13 - processUsingBuffer() function

      // Safe usage of a buffer pointer
      func processUsingBuffer(_ array: [Int]) -> Int {
          array.withUnsafeBufferPointer { buffer in
              var result = 0
              for i in 0..<buffer.count {
                  result += calculate(using: buffer, at: i)
              }
              return result
          }
      }
    • 23:34 - Dangerous getPointerToBytes() function

      // Dangerous - DO NOT USE!
      func getPointerToBytes() -> UnsafePointer<UInt8> {
          let array: [UInt8] = Array(repeating: 0, count: 128)
          // DANGER: The next line escapes a pointer
          let pointer = array.withUnsafeBufferPointer { $0.baseAddress! }
          // DANGER: The next line returns the escaped pointer
          return pointer
      }
    • 24:46 - processUsingSpan() function

      // Safe usage of a span
      @available(macOS 16.0, *)
      func processUsingSpan(_ array: [Int]) -> Int {
          let intSpan = array.span
          var result = 0
          for i in 0..<intSpan.count {
              result += calculate(using: intSpan, at: i)
          }
          return result
      }
    • 25:07 - getHiddenSpanOfBytes() function (attempt 1)

      @available(macOS 16.0, *)
      func getHiddenSpanOfBytes() -> Span<UInt8> { }
      // error: Cannot infer lifetime dependence...
    • 25:28 - getHiddenSpanOfBytes() function (attempt 2)

      @available(macOS 16.0, *)
      func getHiddenSpanOfBytes() -> () -> Int {
          let array: [UInt8] = Array(repeating: 0, count: 128)
          let span = array.span
          return { span.count }
      }
    • 26:27 - RawSpan.readByte() method

      @available(macOS 16.0, *)
      extension RawSpan {
        mutating func readByte() -> UInt8? {
          guard !isEmpty else { return nil }
          
          let value = unsafeLoadUnaligned(as: UInt8.self)
          self = self._extracting(droppingFirst: 1)
          return value
        }
      }
    • 28:02 - Final QOIParser.parseQOI(from:) method

      /// Parses an image from the given QOI data.
      mutating func parseQOI(from input: inout RawSpan) -> QOI? {
        guard let header = QOI.Header(parsing: &input) else { return nil }
        
        let totalBytes = header.pixelCount * Int(header.channels.rawValue)
        
        let pixelData = Data(rawCapacity: totalBytes) { outputSpan in
          while outputSpan.count < totalBytes {
            guard let nextPixel = parsePixel(from: &input) else { break }
            
            switch nextPixel {
            case .run(let count):
              for _ in 0..<count {
                previousPixel
                  .write(to: &outputSpan, channels: header.channels)
              }
              
            default:
              decodeSinglePixel(from: nextPixel)
                .write(to: &outputSpan, channels: header.channels)
              
            }
          }
        }
        
        return QOI(header: header, data: pixelData)
      }
    • 28:31 - RGBAPixel.write(to:channels:) method

      @available(macOS 16.0, *)
      extension RGBAPixel {
        /// Writes this pixel's RGB or RGBA data into the given output span.
        @lifetime(&output)
        func write(to output: inout OutputRawSpan, channels: QOI.Channels) {
          output.append(r)
          output.append(g)
          output.append(b)
          
          if channels == .rgba {
            output.append(a)
          }
        }
      }
    • 0:00 - 서론 및 어젠다
    • Swift 코드 앱과 Swift 6.2를 사용하는 라이브러리의 성능을 최적화하는 방법에 대해 알아보세요. 새로운 ‘InlineArray’와 ‘Span’ 유형은 할당, 배타성 검사, 참조 계산을 줄여줍니다. 새로운 오픈 소스 Swift 라이브러리인 Binary Parsing은 빠르고 안전한 바이너리 파싱을 위해 도입되었습니다.

    • 1:19 - QOI 형식 및 파서 앱
    • 이 WWDC25 세션의 앱은 단일 페이지 사양을 갖춰 간단하고 손실 없는 형식인 QOI 형식으로 이미지를 로드합니다. 앱의 이미지 파서는 다양한 픽셀 인코딩 메서드를 처리합니다. 앱은 작은 아이콘 파일을 즉시 로드하지만, 더 큰 새 사진을 로드하는 데는 몇 초가 걸립니다.

    • 2:25 - 알고리즘
    • 앱이 실제 데이터를 다룰 때 알고리즘이나 데이터 구조를 잘못 사용하면 성능 문제가 빈번하게 발생할 수 있습니다. 이러한 문제를 식별하고 해결하려면 할당 및 릴리즈를 분석하고 프로파일러를 사용하여 비효율적인 코드를 식별하는 도구 템플릿이 포함된 Instruments를 사용하면 됩니다. Time Profiler 도구는 성능 문제를 해결하는 데 특히 유용합니다. 캡처된 호출 및 스택 추적을 분석하면 앱이 가장 많은 시간을 소비하는 영역을 정확히 파악할 수 있습니다. 이 예제에서는 ‘platform_memmove’라는 데이터 복사를 위한 시스템 호출 시 상당한 시간이 소모되었습니다. Instruments를 사용하여 이 예제에서는 ‘readByte’라는 사용자 정의 메서드를 분석합니다. 이러한 메서드는 ‘Data’ 유형의 확장 프로그램에 추가되어 이진 데이터가 과도하게 복사되었습니다. 이 예제에서는 기존 메서드를 더 효율적인 ‘popFirst()’ 메서드로 대체하여 복사하지 않고도 시퀀스 앞에서 데이터를 줄입니다. 이러한 변화로 ‘readByte’ 메서드의 성능 문제가 해결되었습니다. 해당 변화가 적용된 후, 이 예제에서는 프로필을 재실행했고 플레임 그래프에서 중요한 ‘platform_memmove’ 바가 사라졌습니다. 벤치마킹 결과, 상당한 속도가 향상되었고 이미지 크기와 구문 분석 시간 간의 관계가 보조에서 기본으로 바뀌어 알고리즘의 효율성이 높아졌습니다.

    • 8:17 - 할당
    • 이 앱을 다시 프로파일링하면 이미지 파서가 특히 배열과 관련된 과도한 메모리 할당 및 할당 해제를 발생시킨다는 사실을 알 수 있습니다. 단일 이미지를 구문 분석하는 데 거의 백만 개가 할당된다는 것은 심각한 문제가 있다는 것을 나타냅니다. 이러한 할당의 대부분은 일시적이고 단기간 지속되기 때문에 최적화할 수 있다는 점을 시사합니다. 이처럼 불필요한 할당의 출처를 파악하기 위해 이 예제에서는 Instruments의 할당 도구를 사용합니다. 분석 결과, ‘RGBAPixel.data(channels:)’라는 메서드가 주요 원인인 것으로 밝혀졌습니다. 이 메서드는 호출될 때마다 배열을 생성하기 때문에 상당 수의 할당이 발생합니다. ‘flatMap’과 ‘prefix’ 메서드의 복잡한 체인을 포함하는 코드 구조가 문제를 악화시킵니다. 이 체인의 각 단계에서는 배열이 반복적으로 생성, 평면화, 복사되기 때문에 새로운 할당이 발생합니다. 이러한 접근 방식은 간결하지만, 메모리 효율은 높지 않습니다. 이러한 문제를 해결하기 위해 이 예제에서는 구문 분석 함수를 다시 작성합니다. 중간 할당에 의존하는 대신, 결과 데이터의 전체 크기를 미리 계산하고 단일 버퍼를 할당합니다. 이러한 접근 방식을 사용하면 디코딩 과정을 진행하는 동안 반복적으로 할당할 필요가 없습니다.

    • 16:30 - 독점성
    • 앱의 성능이 크게 향상되어 프로파일링 도구에 더 많은 데이터가 필요했습니다. 파싱 코드를 50번 반복한 후, 배타성 테스트를 나타내는 ‘swift_beginAccess’와 ‘swift_endAccess’ 기호가 결과에 나타났습니다. 이러한 배타성 테스트는 ‘QOIParser’ 구조 내에 중첩된 ‘State’ 클래스의 속성으로 인해 발생했는데, 이 예제에서는 배타성 검사를 제거하기 위해 이를 부모 파서 유형으로 직접 옮깁니다. 몇 가지 컴파일러 조정 후, 새로운 프로필 실행을 통해 검증한 결과 독점성 검사가 완전히 제거되었습니다.

    • 19:12 - 스택 및 힙 비교
    • 이 예제에서는 앱의 ‘Array’ 사용을 인라인으로 저장된 고정 크기 컬렉션인 ‘InlineArray’로 대체하여 참조 계산 및 배타성 검사를 제거함으로써 메모리 사용을 최적화합니다. 이는 크기가 절대 변하지 않고 그 자리에서 수정되는 64개 요소 배열인 픽셀 캐시에 적합하여 참조를 복사하거나 공유할 필요 없이 성능을 향상시킵니다.

    • 21:08 - 레퍼런스 카운팅
    • 앱의 마지막 최적화 예제에서는 새로운 ‘Span’ 유형을 사용하여 성능을 개선하고 메모리 안전성을 강화합니다. Instruments에서는 Time Profiler 분석의 플레임 그래프가 사용됩니다. 프로파일링된 데이터는 ‘QOIParser’에 초점을 맞추고 있는데, 특히 ‘Data’ 유형이 copy-on-write 의미론으로 인해 참조 계산 작업에 상당한 시간을 소모하는 것으로 나타났습니다. ‘Span’ 및 관련 유형은 컬렉션에서 연속된 메모리를 다루는 새로운 방식입니다. Swift의 탈출 불가능(‘~Escapable’) 기능을 사용하는데, 이 기능은 컬렉션에 수명을 바인딩하여 메모리 안전을 보장하고 수동 메모리 관리의 필요성을 제거합니다. 이를 통해 안전하지 않은 포인터 관련 위험 없이 메모리에 효율적으로 액세스할 수 있습니다. 이 예제에서는 ‘Span’ 유형을 사용하여 기존 메서드를 다시 작성하여 더 간단하고, 더 안전하며, 더 나은 성능을 내는 방법을 보여 줍니다. 이미지 파싱 메서드에서 ‘Data’는 ‘RawSpan’으로 대체되고 참조 계산의 오버헤드가 크게 줄어듭니다. 그뿐만 아니라, 파싱 프로세스에 ‘OutputSpan’을 도입하여 최적화 기능을 더욱 높여 안전하지 않은 포인터를 사용하지 않고도 파싱 작업을 이전보다 6배 더 빠르게 수행할 수 있습니다.

    • 29:52 - Swift Binary Parsing 라이브러리
    • Swift Binary Parsing을 사용하면 바이너리 형식에 대한 안전하고 효율적인 파서를 만들 수 있습니다. 정수 오버플로 방지, 부호 여부, 비트 폭, 바이트 순서 지정, 사용자 정의 유형 검증 등 다양하게 안전 측면을 처리하는 도구를 제공합니다. 이 라이브러리는 이미 Apple에서 사용 중이고 Swift 포럼과 GitHub을 통해 공개적으로 사용하고 이에 기여할 수 있습니다.

    • 31:03 - 다음 단계
    • 주요 내용은 다음과 같습니다. 앱을 프로파일링하기 위해 Xcode 및 Instruments를 사용합니다. 알고리즘의 성능을 분석하여 병목 현상을 파악합니다. Swift 6.2에 도입된 새로운 ‘InlineArray’ 및 ‘Span’ 유형을 사용하여 위의 문제에 대한 솔루션을 살펴봅니다.

Developer Footer

  • 비디오
  • WWDC25
  • Swift로 메모리 사용량 및 성능 개선하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드(영문)
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    Apple Developer 앱 받기
    Copyright © 2025 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침