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·코드 사이즈·디버깅성을 함께 체크하세요.
'Dev Study > Swift' 카테고리의 다른 글
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
|---|---|
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |
| Swift에서 문자열 비교 (0) | 2025.11.11 |
| Swift에서 ARC 최적화: weak vs unowned (0) | 2025.11.11 |
| Swift에서 구조체(Value Type) 기반 설계 (0) | 2025.11.11 |
| Swift에서 switch문의 pattern matching (0) | 2025.11.11 |
| Swift에서 문자열 결합을 위해 joined(separator:) 사용하기 (0) | 2025.11.11 |
| Swift에서 guard let vs 중첩 if let (0) | 2025.11.11 |


