Scala3 - inline
스칼라 3에는 inline이라는 새로운 키워드가 도입되었습니다. 이 키워드는 매크로와 구별되면서도 컴파일 타임에 일부 코드 조작을 수행할 수 있는 메타프로그래밍 개념을 가집니다.
inline?
inline은 다른 언어에서 오래 전에 등장한 키워드입니다. 컴파일러가 함수에 대한 모든 호출을 함수의 본문 내용으로 채워지도록 합니다. 이렇게 하면 함수 호출이 없으므로 애플리케이션의 성능이 향상될 수 있습니다. 그러나 다른 한편으로는 실행 파일이 커질 수 있어 개념을 잘 알고 사용해야 함을 의미 합니다.
컴파일러가 함수 호출 본문을 채워준다는게 무슨 말인지 직접 확인해볼 간단한 예제를 만들어 보겠습니다.
우선 inline
이 아닌 경우를 먼저 살펴 봅니다.
object NotInlineFunc:
def multiply(a: Int, b: Int): Int = a * b
def main(args: Array[String]): Unit =
val result = multiply(1, 2)
println(s"Result: $result")
scala-cli compile 할때 -Xprint:inlining 옵션을 주면 컴파일러가 만든 코드를 볼 수 있습니다.
scala-cli compile NotInlineFunc.scala -Xprint:inlining
package <empty> {
final lazy module val NotInlineFunc: NotInlineFunc = new NotInlineFunc()
@SourceFile("NotInlineFunc.scala") final module class NotInlineFunc() extends
Object() { this: NotInlineFunc.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(classOf[NotInlineFunc.type])
def multiply(a: Int, b: Int): Int = a.*(b)
def main(args: Array[String]): Unit =
{
val result: Int = NotInlineFunc.multiply(1, 2)
println(
_root_.scala.StringContext.apply(["Result: ","" : String]*).s(
[result : Any]*)
)
}
}
}
NotInlineFunc.multiply(1, 2) 함수 호출을 하고 있는것을 알 수 있습니다.
그럼 이번엔 inline을 쓰기 위해 def multiply 앞에 넣어 줍니다.
object InlineFunc:
inline def multiply(a: Int, b: Int): Int = a * b
def main(args: Array[String]): Unit =
val result = multiply(1, 2)
println(s"Result: $result")
scala-cli compile InlineFunc.scala -Xprint:inlining
[[syntax trees at end of inlining]]
package <empty> {
final lazy module val InlineFunc: InlineFunc = new InlineFunc()
@SourceFile("InlineFunc.scala") final module class InlineFunc() extends Object
() { this: InlineFunc.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(classOf[InlineFunc.type])
inline def multiply(a: Int, b: Int): Int = a.*(b):Int
def main(args: Array[String]): Unit =
{
val result: Int = 2:Int
println(
_root_.scala.StringContext.apply(["Result: ","" : String]*).s(
[result : Any]*)
)
}
}
}
multiply(1,2) 함수를 호출한곳에 2:Int 로 바뀌어 있는것을 확인할 수 있습니다. 컴파일러가 함수 호출이 아닌 multiply 함수를 호출한 결과를 채워준 것 입니다.
재귀적 인라인 메서드
인라인 메서드는 재귀적으로 호출될 수 있으며, 컴파일 타임에 재귀 호출이 실제 코드로 대체됩니다.
object Factorial:
inline def factorial(n: Int): Int =
if n == 0 then 1
else n * factorial(n - 1)
@main def runRecursiveInline() =
val num = 5
val result = Factorial.factorial(num)
println(s"The factorial of $num is $result")
factorial
함수는 재귀적이며 인라인으로 선언되었습니다.컴파일 타임에 재귀 호출이 풀려 실제 곱셈 연산으로 대체됩니다.
오버라이딩 규칙
인라인 메서드는 비인라인 메서드를 오버라이드할 수 있습니다.
abstract class Greeter:
def greet(name: String): String
class FriendlyGreeter extends Greeter:
inline def greet(name: String): String = s"Hello, $name!"
@main def runInlineOverride() =
val greeter: Greeter = new FriendlyGreeter()
println(greeter.greet("Scala"))
FriendlyGreeter
클래스에서 인라인 메서드greet
를 오버라이드합니다.Greeter
타입의 변수로 호출해도 인라인된 메서드가 호출됩니다.
@inline
과의 관계
Scala 2의 @inline
어노테이션은 컴파일러에게 인라이닝을 요청하는 힌트지만, 실제로 인라인될지는 보장되지 않습니다. Scala 3의 inline
키워드는 인라이닝을 보장합니다.
object Calculator:
@deprecated("Use inline multiply instead", "1.0")
@inline def multiplyOld(a: Int, b: Int): Int = a * b
inline def multiply(a: Int, b: Int): Int = a * b
@main def runInlineVsAnnotation() =
val resultOld = Calculator.multiplyOld(3, 4)
val resultNew = Calculator.multiply(3, 4)
println(s"Old Multiply Result: $resultOld")
println(s"New Multiply Result: $resultNew")
scala-cli InlineFunc.scala -Xprint:inlining 18:11:24
package <empty> {
final lazy module val Calculator: Calculator = new Calculator()
@SourceFile("InlineFunc.scala") final module class Calculator() extends Object
() { this: Calculator.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(classOf[Calculator.type])
@inline @deprecated("Use inline multiply instead", "1.0") def multiplyOld(
a: Int, b: Int): Int = a.*(b)
inline def multiply(a: Int, b: Int): Int = a.*(b):Int
}
final lazy module val InlineFunc$package: InlineFunc$package =
new InlineFunc$package()
@SourceFile("InlineFunc.scala") final module class InlineFunc$package()
extends Object() { this: InlineFunc$package.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(
classOf[InlineFunc$package.type])
@main def runInlineVsAnnotation(): Unit =
{
val resultOld: Int = Calculator.multiplyOld(3, 4)
val resultNew: Int = 12:Int
println(
_root_.scala.StringContext.apply(
["Old Multiply Result: ","" : String]*).s([resultOld : Any]*)
)
println(
_root_.scala.StringContext.apply(
["New Multiply Result: ","" : String]*).s([resultNew : Any]*)
)
}
}
@SourceFile("InlineFunc.scala") final class runInlineVsAnnotation() extends
Object() {
<static> def main(args: Array[String]): Unit =
try runInlineVsAnnotation() catch
{
case error @ _:scala.util.CommandLineParser.ParseError =>
scala.util.CommandLineParser.showError(error)
}
}
}
@inline
은 힌트일 뿐이며,inline
키워드는 인라이닝을 보장합니다.컴파일 시
multiplyOld
는 인라인되지 않을 수 있습니다.
상수 표현식의 정의
inline val
의 우변은 컴파일 타임에 결정되는 상수 표현식이어야 합니다.
object Constants:
inline val PI = 3.14159
inline val TWO_PI = PI * 2
@main def runConstantExpression() =
println(s"PI: ${Constants.PI}")
println(s"Two PI: ${Constants.TWO_PI}")
PI
와TWO_PI
는 컴파일 타임에 계산되어 상수로 취급됩니다.상수 표현식은 리터럴 또는 컴파일 타임에 계산 가능한 식이어야 합니다.
Transparent Inline Methods
transparent inline
메서드는 반환 타입이 실제 반환 값에 따라 결정됩니다. 이를 통해 컴파일 타임에 더 구체적인 타입 정보를 얻을 수 있습니다.
transparent inline def getValue(inline flag: Boolean) =
if flag then 42 else "Scala"
@main def runTransparentInline() =
val intValue = getValue(true)
val stringValue = getValue(false)
println(s"Int Value: $intValue (type: ${intValue.getClass})")
println(s"String Value: $stringValue (type: ${stringValue.getClass})")
scala-cli InlineFunc.scala -Xprint:inlining 18:11:37
Compiling project (Scala 3.4.2, JVM (11))
[[syntax trees at end of inlining]] // /Users/bench87/Documents/Developments/scala/InlineFunc.scala
package <empty> {
final lazy module val InlineFunc$package: InlineFunc$package =
new InlineFunc$package()
@SourceFile("InlineFunc.scala") final module class InlineFunc$package()
extends Object() { this: InlineFunc$package.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(
classOf[InlineFunc$package.type])
inline transparent def getValue(inline flag: Boolean): Int | String =
if flag then 42 else "Scala"
@main def runTransparentInline(): Unit =
{
val intValue: Int = 42
val stringValue: String = "Scala"
println(
_root_.scala.StringContext.apply(
["Int Value: "," (type: ",")" : String]*).s(
[intValue,intValue.getClass[Int]() : Any]*)
)
println(
_root_.scala.StringContext.apply(
["String Value: "," (type: ",")" : String]*).s(
[stringValue,stringValue.getClass[String]() : Any]*)
)
}
}
@SourceFile("InlineFunc.scala") final class runTransparentInline() extends
Object() {
<static> def main(args: Array[String]): Unit =
try runTransparentInline() catch
{
case error @ _:scala.util.CommandLineParser.ParseError =>
scala.util.CommandLineParser.showError(error)
}
}
}
Compiled project (Scala 3.4.2, JVM (11))
Int Value: 42 (type: int)
String Value: Scala (type: class java.lang.String)
getValue
함수는transparent inline
으로 선언되어 반환 타입이 호출 시점에서 결정됩니다.intValue
의 타입은Int
,stringValue
의 타입은String
으로 추론됩니다.
인라인 조건문
inline if
를 사용하면 조건이 상수 표현식이어야 하며, 컴파일 타임에 조건이 평가되어 불필요한 코드가 제거됩니다.
inline val DEBUG = true
inline def debugLog(message: String) =
inline if DEBUG then println(s"DEBUG: $message")
@main def runInlineConditional() =
debugLog("This is a debug message")
println("Program executed")
scala-cli InlineFunc.scala -Xprint:inlining 18:21:06
Compiling project (Scala 3.4.2, JVM (11))
[[syntax trees at end of inlining]] // /Users/bench87/Documents/Developments/scala/InlineFunc.scala
package <empty> {
final lazy module val InlineFunc$package: InlineFunc$package =
new InlineFunc$package()
@SourceFile("InlineFunc.scala") final module class InlineFunc$package()
extends Object() { this: InlineFunc$package.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(
classOf[InlineFunc$package.type])
inline val DEBUG: (true : Boolean) = true
inline def debugLog(message: String): Unit =
(inline if DEBUG then
println(
_root_.scala.StringContext.apply(["DEBUG: ","" : String]*).s(
[message : Any]*)
)
else ()):Unit
@main def runInlineConditional(): Unit =
{
println(
_root_.scala.StringContext.apply(["DEBUG: ","" : String]*).s(
["This is a debug message" : Any]*)
):Unit
println("Program executed")
}
}
@SourceFile("InlineFunc.scala") final class runInlineConditional() extends
Object() {
<static> def main(args: Array[String]): Unit =
try runInlineConditional() catch
{
case error @ _:scala.util.CommandLineParser.ParseError =>
scala.util.CommandLineParser.showError(error)
}
}
}
Compiled project (Scala 3.4.2, JVM (11))
DEBUG: This is a debug message
Program executed
- DEBUG println이 추가 되어 있습니다.
DEBUG = false로 바꿔 보겠습니다.
scala-cli InlineFunc.scala -Xprint:inlining 18:22:24
Compiling project (Scala 3.4.2, JVM (11))
[[syntax trees at end of inlining]] // /Users/bench87/Documents/Developments/scala/InlineFunc.scala
package <empty> {
final lazy module val InlineFunc$package: InlineFunc$package =
new InlineFunc$package()
@SourceFile("InlineFunc.scala") final module class InlineFunc$package()
extends Object() { this: InlineFunc$package.type =>
private def writeReplace(): AnyRef =
new scala.runtime.ModuleSerializationProxy(
classOf[InlineFunc$package.type])
inline val DEBUG: (false : Boolean) = false
inline def debugLog(message: String): Unit =
(inline if DEBUG then
println(
_root_.scala.StringContext.apply(["DEBUG: ","" : String]*).s(
[message : Any]*)
)
else ()):Unit
@main def runInlineConditional(): Unit =
{
():Unit
println("Program executed")
}
}
@SourceFile("InlineFunc.scala") final class runInlineConditional() extends
Object() {
<static> def main(args: Array[String]): Unit =
try runInlineConditional() catch
{
case error @ _:scala.util.CommandLineParser.ParseError =>
scala.util.CommandLineParser.showError(error)
}
}
}
Compiled project (Scala 3.4.2, JVM (11))
Program executed
DEBUG
가false
이므로debugLog
함수의 내용은 컴파일 타임에 제거됩니다.
Inline match
inline match
를 사용하면 컴파일 타임에 패턴 매칭이 결정되어 분기가 선택됩니다.
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
transparent inline def area(shape: Shape): Double =
inline shape match
case Circle(r) => Math.PI * r * r
case Rectangle(w, h) => w * h
@main def runInlineMatch() =
val circleArea = area(Circle(5))
val rectangleArea = area(Rectangle(4, 6))
println(s"Circle Area: $circleArea")
println(s"Rectangle Area: $rectangleArea")
area
함수는transparent inline
과inline match
를 사용하여 컴파일 타임에 패턴 매칭이 결정됩니다.각 도형에 맞는 면적 계산이 컴파일 타임에 결정되어 효율적인 코드를 생성합니다.
참고 자료 (Reference)
Subscribe to my newsletter
Read articles from 최명규 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by