인라인 함수
인라인 함수 (Inline Functions)
인라인 함수는 말 그대로 호출하는 코드에 직접 통합되는 함수예요. 일반 함수는 호출 지점에서 함수 본문으로 점프해서 실행되지만, 인라인 함수는 컴파일러가 함수 본문을 호출한 자리 그대로 심어 넣어요. 이 페이지에서는 어떤 상황에서 인라인 함수가 필요하고, 언제 주의해야 하는지 하나씩 살펴볼게요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/inline-functions
본문
인라인 함수 사용하기 (Using Inline Functions)
정적 형식 매개 변수(static type parameter)를 사용할 때는, 형식 매개 변수로 매개 변수화된 함수는 반드시 inline이어야 해요. 그래야 컴파일러가 이 형식 매개 변수들을 해석할 수 있거든요. 반면 일반적인 제네릭 형식 매개 변수(ordinary generic type parameter)를 사용할 때는 그런 제약이 없어요.
인라인 함수는 멤버 제약 조건(member constraint)을 쓸 수 있게 해 주는 것 외에도, 코드 최적화에 도움이 되기도 해요. 하지만 인라인 함수를 남용하면 코드가 컴파일러 최적화나 라이브러리 함수 구현의 변화에 덜 견고해질 수 있어요. 그래서 다른 최적화 기법을 모두 시도해 보기 전에는, 최적화 목적으로 인라인 함수를 쓰는 걸 피하는 게 좋아요. 함수나 메서드를 inline으로 만들면 성능이 좋아질 때도 있지만, 항상 그런 건 아니에요. 따라서 특정 함수를 인라인으로 만드는 게 실제로 긍정적인 효과가 있는지 성능 측정으로 확인해 보는 것도 필요해요.
inline 한정자는 최상위(top level) 함수, 모듈 수준 함수, 또는 클래스 안의 메서드 수준에도 적용할 수 있어요.
다음 코드 예시는 최상위 인라인 함수, 인라인 인스턴스 메서드, 인라인 정적 메서드를 각각 보여 줘요.
let inline increment x = x + 1
type WrapInt32() =
member inline this.incrementByOne(x) = x + 1
static member inline Increment(x) = x + 1
인라인 함수와 형식 유추 (Inline Functions and Type Inference)
inline의 존재 여부는 형식 유추(type inference)에도 영향을 줘요. 인라인 함수는 정적으로 해석되는 형식 매개 변수(statically resolved type parameter)를 가질 수 있는 반면, 일반 함수는 그럴 수 없기 때문이에요. 아래 코드는 float 변환 연산자처럼, 정적으로 해석되는 형식 매개 변수를 쓰는 함수를 사용할 때 inline이 얼마나 유용한지를 보여 줘요.
let inline printAsFloatingPoint number =
printfn "%f" (float number)
inline 한정자가 없다면, 형식 유추는 이 함수가 특정 형식(여기서는 int)을 받도록 강제해요. 하지만 inline 한정자가 있으면 함수도 정적으로 해석되는 형식 매개 변수를 갖도록 유추돼요. inline 한정자를 쓴 경우 형식은 다음과 같이 유추돼요.
^a -> unit when ^a : (static member op_Explicit : ^a -> float)
즉, 이 함수는 float로의 변환을 지원하는 아무 형식이나 받아들일 수 있다는 뜻이에요.
InlineIfLambda
F# 컴파일러에는 코드를 인라인 처리하는 최적화기가 포함되어 있어요. InlineIfLambda 특성은 코드가 선택적으로 나타낼 수 있는데, 인자가 람다 함수로 판명되면 그 인자 자체를 호출 지점에서 항상 인라인 처리하도록 하는 거예요. 자세한 내용은 F# RFC FS-1098을 참고하세요.
예를 들어 배열을 순회하는 다음 iterateTwice 함수를 생각해 볼게요.
let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
for i = 0 to array.Length-1 do
action array[i]
for i = 0 to array.Length-1 do
action array[i]
호출 지점이 다음과 같다면,
let arr = [| 1.. 100 |]
let mutable sum = 0
arr |> iterateTwice (fun x ->
sum <- sum + x)
인라인 및 기타 최적화를 거친 뒤 코드는 다음과 같이 바뀌어요.
let arr = [| 1..100 |]
let mutable sum = 0
for i = 0 to arr.Length - 1 do
sum <- sum + arr[i]
for i = 0 to arr.Length - 1 do
sum <- sum + arr[i]
이 최적화는 관련된 람다 표현식의 크기에 관계없이 적용돼요. 이 기능은 루프 언롤링(loop unrolling)이나 이와 유사한 변환을 더 안정적으로 구현할 때도 활용할 수 있어요.
옵트인(opt-in) 경고(/warnon:3517 또는 <WarnOn>3517</WarnOn> 속성)를 켜면, 코드에서 InlineIfLambda 인자가 호출 지점의 람다 표현식에 묶이지 않은 곳을 알려 줘요. 일반적인 상황에서는 이 경고를 켜지 않는 게 좋아요. 다만 특정 종류의 고성능 프로그래밍에서는 모든 코드가 인라인되고 평평해지도록 보장하는 게 유용할 수 있어요.