본문 바로가기
WWDC/2024

Analyze heap memory

by 찜토끼 2024. 8. 8.

https://developer.apple.com/videos/play/wwdc2024/10173/

 

Analyze heap memory - WWDC24 - Videos - Apple Developer

Dive into the basis for your app's dynamic memory: the heap! Explore how to use Instruments and Xcode to measure, analyze, and fix common...

developer.apple.com

해당 포스트는 위 영상을 기반으로 개인적으로 메모/정리한 글입니다.

개요

Heap - dynamic memory- 의 기초를 살펴보자. Heap memory 를 측정하고 줄일 수 있는 방법을 알아보자.


Heap

malloc() 에 의해 할당되는 메모리.

참조 유형이 저장되는 곳.

memory limit 이슈를 발생시킬 수 있는 곳.

앱의 Virtual memory
각 페이지는 세 개중 하나의 상태를 가짐 → clean, dirty, swapped

  • clean: 아직 쓰여지지 않은 메모리. 할당되고 사용하되지 않은 메모리 or 읽기 전용 맵핑
  • dirty: 최근에 읽힌 메모리. 메모리 워닝 시 압축(swapped)되거나 디스크에 쓰여질 수 있음
  • swapped: 압축된 상태

 

Measuring your heap

MallocStackLogging

enabled 시켜야 track 가능

 

Xcode Memory Report

 

Memory Graph Debugger

MallocStackLogging 을 켜두면 각 allocation의 backtraces 도 트랙킹 가능하다

 

CLI Tools

커맨드 라인 툴. Leaks, heap, vmmap, malloc_history 파악 가능

 

Instrunments

  • Allocations: 모든 alloc 의 히스토리를 기록한다
  • VM Tracker: Allocations 에 포함됨. 모든 가상 메모리의 시간에 따른 스냅샷을 찍는다
  • Leaks: app 메모리의 시간경과에 따른 스냅샷을 찍는다

예시: 메모리가 특정 액션 실행 시마다 스파이크 치는 현상 발견!

 

Dealing with transient growth

순간적으로 증가했다 감소하는 메모리를 해결하기.

메모리 스파이크를 주의해야 하는 이유
- 메모리 Pressure 를 유발시켜서 앱의 성능에 영향을 줌(ex: 백그라운드 테스크 종료 등)
- OOM 크래시
- Heap memory regions 에 fragmentation 이나 hole을 유발함

두 가지 측정 방법이 있다: 부분적으로 살펴보기 or 전체를 살펴보기

 

트랙킹 예시

Malloc 의 Call Tree 를 확인 → 8GB나 되는 temp alloc 을 확인할 수 있음 → makeThumbnail() 이 Crue
Call Tree 를 따라가보면 여기서 For-loop 를 돌며 임시메모리 증가를 확인할 수 있다

Autorelease pool
- Swift 에서 알아서 관리해주는 임시 오브젝트 관리소 같은 거. 이게 보통 temp 메모리 증가를 유발한다
- (ex) `print(”Now is \(Date.now)”)` 코드에서 .description 을 위한 autoreleased String 이 생성되고 이것이 heap에 들어감.
- loop 에서 대부분 문제가 된다. autorelease pool 의 scope 가 끝나야 release 를 하기 때문에 loop 가 끝날 때 까지는 계속 heap 에 쌓이게 되기 때문

위와 같은 이슈를 해결하는 방법 → autoreleasepool 의 scope 를 명시적으로 지정해주면 된다.

(모든 loop 가 끝날 때까지 굳이 String 을 heap 에 유지할 필요가 없다.)

 

Tracking persistent growth

지속적으로 높아지는 메모리를 추적해보자.

시간이 경과함에 따라 계속 증가하는 allocation

Generations 옵션을 선택하면 Generation 의 스냅샷을 찍을 수 있다.

스냅샷을 펼치면 어떤 것들이 allocation 되었는지 size 로 소팅해서 볼 수도 있다
우측의 Stack Trace 를 보면 힌트를 발견할 수 있다
이 중 하나의 address 를 복사해서 Memory Graph Debugger 에서 필터해보자!
하단 필터바에서 address 를 입력함으로써 필터 가능함
어디서 이 객체를 잡고 있는지 따라가보면 결국 Global Image Cache 가 나온다 (계속 새로 캐싱하는 버그가 있었음)

 

Memory graph debugger

Why is this object still around” 를 대답해주는 툴.

제비가 코코넛에 대한 Strong Reference를 가지고 있다 (강한 참조 중)

MallocStackLogging 을 켜두면 각 allocation의 backtraces 도 트랙킹 가능하다 (매우 도움)

 

Fixing memory leaks

Reachability → 모든 메모리는 non-weak 참조를 통해 도달(reachable)할 수 있어야 한다.

Heap에서의 메모리 종류

  • Useful memory: 미래에 다시 쓰일 Reachable allocations
  • Abandoned memory: 다시 쓰일 일 없는 Reachable allocations. 무분별한 캐싱 등으로 발생
  • Leaked memory: 해제할 수 있는 소유자가 없는 Unreachable allocations.

Memory graph debugger

Navigator 에서 선택 시 문제를 확인 가능하다.

Leak FAQ

  • Q. 왜 툴들이 Leak 감지를 완벽하게 하지 못하나요?
    • A. 툴에서 타입 정보를 알 수 없는 메모리가 많다. 그리고 C언어는 unmangeed pointer 를 허용하기 때문에 툴은 포인터처럼 보이지만 실제로는 아닌 것들을 허용해주어야 한다. 이걸 Conservative scanning (보수적인 스캐닝?)이라고 한다.
  • Q. Leak 개수가 시간에 경과함에 따라 오르락 내리락 하던데 이건 왜 그런가요?
    • A. Heap은 noisy하고 random 하기 때문에 Conservative reference 는 비결정적(non-deterministic)이다. 따라서 leak 발견이 시간이 지남에 따라 늘어날 수도 다시 줄어들 수도 있다.
  • Q. ‘noreturn’ 함수가 leaking 으로 판단될 때가 있는데 이건 왜 이런가요?
    • A. C에서의 `noreturn` 혹은 Swift의 `Never Type return` 을 컴파일러가 호출할 때는 어차피 함수에서 리턴하는 게 없으니까 cleanup을 하게 된다. 그러나 이것을 하지 못할 때 Leak으로 판단된다.

좌측이 Leak 탐지. 우측이 해결책. 글로벌에서 참조를 들고있음을 인식할 수 있기 때문에 Leak 취급이 아님

 

Improving runtime performance

weak vs unowned

  • weak: 항상 optional type. destination 이 deinit 될 때 nil이 됨. (nil이 되더라도 문제 없음)

실제로는 Weak reference storage 를 사용(추가 메모리). 런타임 cost 가 든다

  • unowned: non-optional. force-unwrapped weak 같은 거. 직접적으로 destination 을 잡고있기 때문에 weak 보다 cost 면에서 효율적이다. 그러나 weak 와 달리 destination이 deinit 되었을 때 unowned 에 접근하면 Crash/bloat (주의할 것!!)

weak 와는 달리 다른 메모리가 필요하지 않다
generator closure 가 destination(self, ByteProducer instance)과 같은 lifetime 을 가지기 때문에

결론: Weak 는 Default 로 쓰기 안전하다. Unowned 는 Memory/Runtime Cost를 아껴준다(하지만 안전을 확신할 수 없는 경우 Crash를 유발하니 쓰지말기!).

Safety and performance

참고: weak, unowned 등의 정보를 memory graph 에서 볼 수 없다면 Xcode 셋팅에서 Reflection Metadata Level 이 default 로 잘 설정되어 있는지 확인해보기.

 

유의할 점

Performance 를 측정하기 위한 관찰을 시작하면 그 자체로도 Observation cost 가 발생한다. 예를 들어 MallocStackLogging 을 활성화하면 오직 그 로깅 때문에 memory 를 씀. Memory Graph Debugger 로 snapshot을 찍으면 Suspend 가 발생함 등등.