• swift 2.0 performance update
    • swift는 수많은 좋은 기능을 가진 유연하고 안전한 언어다.

      • 클로저, 프로토콜, 제네릭, ARC 등…
    • 근데 이런 많은 기능 때문에 느려지는 게 아니냐고 생각할 수도 있다.

      • 하지만 swift는 고도로 최적화된 네이티브 코드를 내보냄으로써 오버헤드를 최소화 한다.

        스크린샷 2022-08-17 오후 4.00.57.png

    • 모든 최적화를 볼 수 없으니 하나만 보자. Array Bounds Checks Optimizations

      • swift는 배열에서 인덱스 접근이 일어날 때, 범위내에서 일어나는 것을 검사한다.

        // 작성하는 코드
        for i in 0..<n {
        	A[i] ^= 13
        }
        
        // 인덱스 접근이 일어날 때 실제로는 검사한다.
        for i in 0..<n {
        	precondition(i<length)
        	A[i] ^= 13
        }
        
      • 다만 이러한 검사는 속도를 느리게 하고, 다른 최적화(vectorization 등)를 못하게 한다.

      • 그래서 루프마다 검사하는 게 아니라, 루프시작할 때 1번만 검사하도록 한다.

        precondition(n<=length)
        for i in 0..<n {
        	A[i] ^= 13
        }
        
    • 이러한 최적화는 수많은 프로그램과 벤치마크를 추적하면서 유효성을 입증한 전략이다.

    • 최적화 된 코드 뿐 아니라 최적화되지 않은 코드도 빠르게 해야 한다.

      • 개발자가 대부분의 시간을 보내는 것은 거기니까.
    • 이를 위해서 두가지를 했다.

      • swift 런타임 컴포넌트 개선
        • 메모리 할당, 메타데이터 접근 등의 작업을 하는 컴포넌트
      • swift 표준 라이브러리 최적화
    • WMO(Whole Module Optimization)

      • 기본적으로는 파일별로 컴파일 한다.
        • 동시에 컴파일이 가능하다.
        • 업데이트가 필요한 파일만 재컴파일 한다.
        • 다만 최적화는 파일 내부에서만 할 수 있다.
      • WMO를 키면 전체 모듈 단위로 최적화를 하기 때문에 좀 더 공격적인 최적화를 할 수 있다.
        • 하지만 당연히 빌드 속도가 좀 더 느릴 수 밖에 없다.
      • Swift 2에의 개선 사항
        • WMO에 의존하는 새로운 최적화들을 추가했다.
        • 일부 부분을 병렬로 돌리도록 하여 전체적인 시간을 줄였다.
      • Xcode 7부터 build setting에서의 최적화 레벨 선택에서 WMO를 선택할 수 있다.
  • understanding swift performance
    • Reference Counting
      • 컴파일러는 일반적으로는 대부분의 참조 카운팅에 의한 오버헤드를 없앨 수 있다.

      • 하지만 가끔은 여전히 참조 카운팅에 의한 느려짐을 겪는 경우가 있다.

      • 참조 카운팅의 동작

        • 할당이 일어나는 지점이 참조 카운팅이 발생하는 지점이다.
        class C { ... }
        func foo(c: C?) P { ... }
        
        var x: C? = C() // 최초 할당, count = 1
        var y: C? = x // 다른 변수에 할당, count = 2
        foo(y) // 임시 변수를 만들어서 y의 값을 할당, count = 3
        // foo 실행 종료 후 임시 변수 사라짐. count = 2
        y = nil // count = 1
        x = nil // count = 0
        
      • struct는 그 자체적으로는 참조 카운팅이 없지만, 클래스 프로퍼티를 가지면 프로퍼티가 참조 카운팅을 하기 때문에 실질적으로는 참조 카운팅을 하게 된다.

        • CoW를 하는 struct는 내부적으로 참조 카운팅을 한다.
        • 이런식으로 참조 카운팅을 하는 프로퍼티를 엄청 많이 가지고 있는 struct의 경우는 참조 카운팅 오버헤드가 클 수 있다.
        • 이럴 때는 Wrapper class로 감싸는 방법으로 참조 카운팅 횟수를 줄이는 방법도 있다.
          • 다만 이 경우는 value semantic에서 reference semantic으로 바뀌기 때문에, 이로 인한 사이드 이펙트는 주의해야 한다.
    • Generics
      • 제네릭의 동작 원리
        • 코드로는 간단하다.

          func min<T: Comparable>(x: T, y: T) -> T {
          	return y < x ? y : x
          }
          
        • 실제 컴파일러가 내보내는 코드는 좀 더 복잡하다.

          • indirection을 사용하고 있다. 실제로 어떤 타입이 넘어갈 지 모르니까.

          • T가 참조 카운팅이 필요한 타입인지조차 알 수 없기 때문에, 참조 카운팅을 위한 코드도 indirection해서 넣어야 한다.

            • 모든 경우에 대응해야 하기 때문에 보수적일 수 밖에 없다.
            // psuedo-Swift
            
            func min<T: Comparable>(x: T, y: T, FTable: FunctionTable) -> T {
            	let xCopy = FTable.copy(x)
            	let yCopy = FTable.copy(y)
            	let m = FTable.lessThan(yCopy, xCopy) ? y : x
            	FTable.release(x)
            	FTable.release(y)
            	return m
            }
            
        • 이 오버헤드를 없애기 위해서 컴파일러는 generic specialization이라는 최적화 기법을 사용한다.

          • 해당 함수가 호출되는 부분을 찾아서, 구체 타입을 확인 후 해당 구체 타입을 타입 매개변수에 직접 넣어서 불필요한 코드를 없앤다.

          • 이후 호출부를 이 특수화된 함수로 바꾼다.

            func min<Int>(x: Int, y: Int) -> Int {
            	return y < x ? y : x
            }
            
        • 이 최적화는 강력하지만, visibility에 따라서 최적화가 돌아가지 못하는 경우가 많다.

          • 함수 정의와 호출이 다른 파일에서 이뤄지는 경우
        • 대신 WMO가 켜져 있으면 가능하다.

    • Dynamic Dispatch
      • 클래스 상속 관계에서의 메소드 호출

        public class Pet {
        	func noise() { ... }
        	
        	var name: String { ... }
        
        	func noiseImpl()
        }
        
        class Dog: Pet {
        	override func noise() { ... }
        }
        
        func makeNoise(p: Pet) {
        	print("My name is \\(p.name)")
        	p.noise()
        }
        
      • makeNoise 함수에 대해서 컴파일러가 내보내는 코드는 좀 더 복잡하다.

        • 역시 indirection이 쓰인다. 서브클래스에서 오버라이드 했을 수도 있으니까.
        // psuedo-Swift
        func makeNoise(p: Pet) {
        	let nameGetter = Pet.nameGetter(p)
        	print("My name is \\(nameGetter(p))")
        	let noiseMethod = Pet.noiseMethod(p)
        	noiseMethod(p)
        }
        
        • 컴파일러는 현재 호출하는 메소드들이 서브클래스에서 오버라이드되지 않았음이 증명되는 경우에만 직접 호출을 할 수 있다.
          • 상속 관계에서는 final을 써서 이를 컴파일러에 알릴 수 있다.
          • private을 붙여도 더이상 override 할 수 없기 때문에 컴파일러가 direct로 고칠 수 있다.
          • WMO를 키는 경우, internal 클래스에서도 모듈 내에 서브클래스가 없는 경우에 한해 direct로 고칠 수 있다.
    • Swift가 Objective-C보다 빠를 수 있는 이유
      • Objective-C는 근본 자체가 dynamic dispatch인 msgSend를 쓰기 때문에 인라이닝이나 코드 분석등이 불가능하다.
      • Swift는 컴파일러가 더 많은 정보를 가지고 있기 때문에 더 많은 경우에 대해서 dynamic dispatch를 줄일 수 있다.
        • 그러니까 최대한 final을 쓰자.
  • Using Instruments to analyze the performance of swift programs
    • release모드로 놓고 빌드하고, time profiler를 켜보자.
      • CPU 사용량이 폭증하는 부분을 찾아서 어디서 시간을 많이 먹는지 체크하라
      • 가장 시간이 많이 걸린 stack trace를 자동으로 찾아준다.
    • 예시에서는 retain/release나 Generic 연산으로 시간이 대부분 소모되는 케이스