Scala3  - inline

최명규최명규
7 min read

스칼라 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}")
  • PITWO_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
  • DEBUGfalse이므로 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 inlineinline match를 사용하여 컴파일 타임에 패턴 매칭이 결정됩니다.

  • 각 도형에 맞는 면적 계산이 컴파일 타임에 결정되어 효율적인 코드를 생성합니다.

참고 자료 (Reference)

0
Subscribe to my newsletter

Read articles from 최명규 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

최명규
최명규