반응형

Swift에서 @inlinable / @inline(__always)

 

Swift 최적화에서 자주 거론되는 두 주석(Attributes)이 있습니다.

  • @inlinable: 모듈 외부에서도 함수 본문을 볼 수 있게 하여 타 모듈 컴파일러가 인라이닝/최적화할 수 있게 함
  • @inline(__always): “항상 인라인해 달라”는 강한 힌트(보장 아님). 코드 사이즈와 최적화 트레이드오프 존재

두 속성은 목적이 다릅니다. @inlinable은 가시성(외부 모듈 최적화 허용), @inline(__always)는 인라이닝 강도 힌트입니다. 아래에 개념, 규칙, 베스트 프랙티스, 예제 코드를 정리했습니다.

 

1. 한눈에 비교

항목 @inlinable @inline(__always)
목적 외부 모듈에서 본문 가시화(SIL 공개) → 타 모듈이 인라인/전개 가능 현재 컴파일 단위에서 강한 인라인 힌트 제공(반드시 보장 X)
적용 위치 public 또는 @usableFromInline가시성의 API 본문 거의 모든 함수/메서드/연산자(내부도 가능)
필요 보조 속성 내부 구현 타입/심볼엔 @usableFromInline 필요 없음
ABI/Resilience 영향 본문이 외부에 노출되어 resilient 변경 제약  ABI 가시성 변화 없음(최적화 힌트만)
코드 사이즈 호출 사이트에 인라인될 수 있어 커질 수 있음 항상(에 가깝게) 인라인 → 사이즈 급증 위험
사용 권장 작고 빈번히 호출·순수·안정적 구현 핫패스의 매우 짧은 함수에만 신중히
대체/보완 자동 인라이너 + Whole‑Module Optimization @inline(never)로 역히트 제어도 가능

인라이닝은 컴파일러가 자동으로 잘 하는 편입니다. 속성은 필요할 때만 신중히 사용하세요.

 

2. @inlinable 기본 규칙

  • 외부 모듈에서 본문을 볼 수 있게 하여 인라이닝/전개/특수화가 가능해집니다.
  • public API 또는 @usableFromInline 으로 노출된 내부 구현만 본문에서 사용할 수 있습니다.
  • Resilience 제약: 본문이 노출되므로, 시그니처가 같더라도 본문의 의미 변경이 클라이언트 최적화 코드를 망가뜨릴 수 있습니다. “한 번 공개하면 함부로 바꾸기 어려움”을 염두에 두세요.

@usableFromInline가 필요한 이유

@inlinable 함수 본문 안에서 내부 타입/함수/프로퍼티를 사용하려면, 그 내부 심볼을 외부 모듈 인라이닝 시점에 참조 가능해야 합니다. 그래서 다음처럼 지정합니다.

@usableFromInline
struct _Pair<T> {
    @usableFromInline var a: T
    @usableFromInline var b: T
}

@inlinable
public func makePair<T>(_ x: T, _ y: T) -> _Pair<T> {
    _Pair(a: x, b: y)   // 본문이 외부에서 보일 때도 참조 가능
}

 

3. @inline(__always) 기본 규칙

  • 강한 인라인 힌트이지만, 컴파일러가 반드시 인라인하는 것은 아닙니다(디버그 빌드, 코드 사이즈 폭증, 재귀 등에서는 무시될 수 있음).
  • 과도한 사용은 코드 사이즈 증가, i-cache 압박, 빌드 시간 증가를 유발할 수 있습니다.
  • 자동 인라이너가 충분히 똑똑합니다. 정말 필요한 핫패스/작은 래퍼에만 제한적으로 사용하세요.
@inline(__always)
@inlinable // 두 속성 병행도 가능: 외부에서도 보이고, 강한 인라인 힌트
public func square(_ x: Int) -> Int { x &* x }

 

4. 실무 선택 가이드

  • 라이브러리/SDK: 타 모듈 최적화 필요가 있는 작고 안정적인 유틸 @inlinable 고려. 내부 구현심볼엔 @usableFromInline.
  • 앱 내부 모듈: 한 모듈 안에서만 쓰는 함수는 굳이 @inlinable 필요 없음. 자동 인라이너 + Whole‑Module Optimization(-wmo)이 충분히 처리.
  • 핫루프·미세 최적화: 1~3 라인 수준의 단순 수학/비트 연산 래퍼에 한해 @inline(__always)를 신중히.
  • 코드 사이즈 민감(Watch, Widget 등): 인라인 남용 자제. 오히려 @inline(never)로 덩치 큰 함수는 분리.

 

5. 예제 모음


5.1 라이브러리: 경량 수학 유틸 (공개 인라이닝)

// Module: MathKit

@inlinable
public func clamp<T: Comparable>(_ x: T, _ lower: T, _ upper: T) -> T {
    precondition(lower <= upper, "invalid range")
    return min(max(x, lower), upper)
}

// 내부 헬퍼를 본문에서 쓰고 싶을 때
@usableFromInline
internal func _fastMin<T: Comparable>(_ a: T, _ b: T) -> T { a <= b ? a : b }

@inlinable
public func median<T: Comparable>(_ a: T, _ b: T, _ c: T) -> T {
    // 본문이 외부에 노출되므로 내부 심볼은 @usableFromInline
    if a > b { return _fastMin(a, c) }
    else     { return _fastMin(b, c) }
}
  • 클라이언트 모듈은 -O에서 clamp, median 본문을 보고 인라인/레인지 전개/분기예측 최적화를 적용할 수 있습니다.

5.2 핫패스 래퍼: 항상 인라인 힌트

@inline(__always)
@inlinable
public func mulAdd(_ a: Int, _ b: Int, _ c: Int) -> Int {
    a &* b &+ c       // 오버플로 정의적 연산자 사용(&*, &+)
}
  • 주의: 이런 작은 함수라도 남발하면 바이너리가 커질 수 있습니다. 성능 프로파일로 핫패스임을 확인하세요.

5.3 제네릭 특수화와 결합

제네릭은 구체 타입이 정해지면 특수화가 이뤄질 수 있고, @inlinable로 본문을 외부에 보여주면 클라이언트 쪽에서의 특수화+인라이닝 여지가 커집니다.

@inlinable
public func dot(_ a: [Float], _ b: [Float]) -> Float {
    precondition(a.count == b.count)
    var s: Float = 0
    // 전형적 핫 루프: -O에서 벡터화 대상
    for i in a.indices { s &+= a[i] * b[i] }
    return s
}

5.4 디버깅/크래시 로그 고려

인라이닝이 많으면 스택 트레이스가 평평해져 추적이 어려울 수 있습니다. 크래시 분석이 중요한 경로라면 과도한 인라이닝은 피합니다.

5.5 OSLog/트레이싱에선 신중히

로깅/계측 코드까지 인라인되면 코드 사이즈·열화가 발생할 수 있습니다. 로깅 래퍼는 @inline(never) 로 두어 핫패스에 침투하지 않게 만들기도 합니다.

@inline(never)
func _log(_ msg: @autoclosure () -> String) {
    // 비용 큰 포맷/IO
    print(msg())
}

 

6. Resilience(탄력적 설계)와 호환성

  • @inlinable은 본문을 노출하므로, 의미·성능 계약이 클라이언트 바이너리에 굳어질 수 있습니다.
  • 공개 후엔 다음이 까다로워집니다.
    • 알고리즘 변경으로 성능/복잡도 특성이 크게 달라지는 것
    • 본문이 참조하는 내부 구현 심볼의 제거/서명 변경(→ @usableFromInline 안정 유지 필요)
  • 장기 유지 SDK에서는 @inlinable 범위를 최소화하거나, 문서로 행위 불변성을 명확히 하세요.

 

7. 흔한 오해와 주의점

1) “@inline(__always)면 무조건 인라인된다” → 아님. 컴파일러/링커 상황에 따라 무시될 수 있음.

2) “@inlinable이면 빨라진다” → 그럴 수도, 아닐 수도. 오히려 코드 사이즈 증가로 i‑cache에 불리할 수 있음.

3) “모든 유틸은 inlinable” → 공개 API 안정성/크래시 스택 가독성/코드 사이즈를 함께 고려.

4) “디버그 빌드도 효과 동일” → 디버그(-Onone)에서는 최적화가 제한적.

 

8. 테스트/프로파일링 체크리스트

  • 성능 판단은 Release + Whole‑Module Optimization에서 재현
  • Instruments(Time Profiler)  signpost로 핫스팟 확인
  • 바이너리 사이즈(기기별 슬라이스) 점검
  • swiftc -O -wmo / Xcode Build Settings의 Optimization Level 확인

 

9. 실전 템플릿

// LibraryUtils.swift

// 내부 구현 노출 (본문에서 쓰기 위해)
@usableFromInline
internal func _assumeNonEmpty<T>(_ a: [T]) -> Bool { !a.isEmpty }

// 외부 인라이닝 허용 + 짧은 경로
@inlinable
public func head<T>(_ a: [T]) -> T {
    precondition(_assumeNonEmpty(a))
    // 짧고 빈번한 경로이므로 클라이언트 인라인 후보
    return a[0]
}

// 극소 래퍼만 항상 인라인 힌트
@inlinable @inline(__always)
public func inc(_ x: Int) -> Int { x &+ 1 }

 

10. 참고 사항

  • @inline(never): 인라이닝 금지 힌트 (디버깅/사이즈 제어에 유용)
  • @_alwaysEmitIntoClient(언더스코어, 비공식): 본문을 항상 클라이언트 쪽에 복사. 안정성·사이즈 리스크가 커서 일반 개발에는 권장하지 않음. 공용 SDK가 아니면 피하세요.

 

11. 결론

  • 가시성 제어(@inlinable)  강도 힌트(@inline(__always)) 를 구분해서 사용하세요.
  • 작은, 빈번, 안정적인 유틸은 @inlinable 고려.
  • 핫패스의 1~3 라인 래퍼만 @inline(__always) 신중 적용.
  • 성능은 측정 기반으로 판단하고, ABI·코드 사이즈·디버깅성을 함께 체크하세요.
반응형
Posted by 까칠코더
,