Code inlining

on waitingforcode.com

Code inlining

JIT is a tool plenty of magic, often considered by programmers as too low level to take them into account. But a lot of this magic stuff has a great influence on coding practices. One of them concerns methods size and is called inlining.

The first part of this post explains the general idea behind inliling. The second section describes inlining parameters in JVM. The last part shows some real case of inlining through Java code.

Inlining

Function inlining more exactly, is a technique aiming to reduce program execution time. It consists on copy-pasting method body to the place where this method is called. Below snippet shows that:

// Not inlined function
void readText() {
  readHeader();
  // some other methods
}
void readHeader() {
  title = page.getTitle();
  introduction = page.getIntroduction();
  text = title + introduction;
}

// Inlined function - readHeader() 
// content is put inside caller method
void readText() {
  title = page.getTitle();
  introduction = page.getIntroduction();
  text = title + introduction;
  // some other methods
}

Java as programming language will be described in the next part. Apart of it, other languages also have the inlining support. C++ for example uses a special keyword called inline to suggest compiler to inline given method. But it's only a suggestion - it's compiler which has the final word. It's important because inline is not always a positive thing. Of course, it reduces method invocation overhead but it makes compiled code heavier. And too big code size can lead to its incapacity to fit in cache. It's especially true in environments where memory constraints are strong and big executable program is not accepted (such as embedded systems).

Bytecode inlining

Inlining in Java is guaranteed by JIT compilation. It achieves inlining thanks to analyze of called methods. For each of them the compilator stores stats indicating how often is used. If given method is invoked very often, it's considered by compilator as a hot method and becomes candidate for inlining. But it's only a candidate because other factors are taken into account, such as: method size (both for caller and callee), inlining depth or method signature.

JVM has parameters helping to parametrize inlining:

  • -XX:MaxInlineSize - maximum bytecode size of a method to be inlined.
  • -XX:FreqInlineSize - maximum bytecode size of a frequently executed method to be inlined.
  • -XX:InlineSmallCode - only if the size of compiled method is smaller than the value specified here, this method will be inlined.
  • -XX:MaxInlineLevel - maximum number of nested calls to be inlined.
  • -XX:MaxTrivialSize - maximum size of trivial method to be inlined.
  • -XX:+PrintInlining - prints inlined methods. If defined, it must be accompanied with XX:+UnlockDiagnosticVMOptions.

Trivial method

Trivial method represents a very simple method, such as for example:

    public int getThreshold() {
      return 3;
    }
    
The size of trivial method is bound by -XX:MaxTrivialSize argument. By default it's 6 bytes.

Inlining can be turned off with -XX:-Inline parameter. But since it's there to improve performance, it can be dangerous to turn it off.

Inlining example

Let's see inlining in real example. The first code shows pretty simple method detecting if passed integer is a pair number. The second case defines more complicated method doing the same thing. For the first time, both codes will be executed with the same JVM arguments: -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining. Below the content of these classes:

// Simple one
public static void main(String[] args) {
  List<Integer> numbers = IntStream.rangeClosed(0, 400000).boxed()
          .collect(Collectors.toList());
  SoftWorker softWorker = new SoftWorker();
  softWorker.work(numbers);
}


private static class SoftWorker {

  public void work(List<Integer> numbers) {
    for (int i = 0; i < numbers.size(); i++) {
      int number = numbers.get(i);
      if (isPair(number)) {
        // do something here
      }
    }
  }

  private boolean isPair(int number) {
    return number%2 == 0;
  }
}

// More complicated
public static void main(String[] args) {
  List<Integer> numbers = IntStream.rangeClosed(0, 400000).boxed()
    .collect(Collectors.toList());
  HardWorker hardWorker = new HardWorker();
  hardWorker.work(numbers);
}

private static class HardWorker {

  public void work(List<Integer> numbers) {
    for (int i = 0; i < numbers.size(); i++) {
      int number = numbers.get(i);
      if (isPair(number)) { 
      }
    }
  }

  private boolean isPair(int number) {
    // Unlike SoftWorker's isPair(int) method,
    // this one contains a little bit more code not necessarily
    // related to pair number detection. It's here to detect if
    // a long method can be inlined
    String stringNumber = String.valueOf(number);
    int divRest;
    if (stringNumber.length() == 3) {
      divRest = number % 10;
    } else if (stringNumber.length() == 4) {
      divRest = number % 100;
    } else if (stringNumber.length() == 5) {
      divRest = number % 1000;
    } else if (stringNumber.length() == 6) {
      divRest = number % 1000;
    } else {
      divRest = number % 10000;
    }
    if (divRest == 0) {
        return true;
    }
    return number % 2 == 0;
  }
}

Their execution with -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining shows that both isPair(int) method were inlined:

@ 22   java.lang.Integer::intValue (5 bytes)
@ 28   com.waitingforcode.compilation.InlineTest$SoftWorker::isPair (12 bytes)
@ 4   java.util.List::size (0 bytes)   no static binding
@ 14   java.util.List::get (0 bytes)   no static binding
@ 22   java.lang.Integer::intValue (5 bytes)
@ 28   com.waitingforcode.compilation.InlineTest$SoftWorker::isPair (12 bytes)
@ 22   java.lang.Integer::intValue (5 bytes)   accessor
@ 28   com.waitingforcode.compilation.InlineTest$SoftWorker::isPair (12 bytes)   inline (hot)

For the case of more complicated class, the inlining was a little bit painfull:

@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
// some other output
@ 22   java.lang.Integer::intValue (5 bytes)
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
// some other output
@ 22   java.lang.Integer::intValue (5 bytes)   accessor
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   inline (hot)
  @ 1   java.lang.String::valueOf (5 bytes)   inline (hot)
    @ 1   java.lang.Integer::toString (48 bytes)   inline (hot)
      @ 24   java.lang.Integer::stringSize (21 bytes)   inline (hot)
      @ 35   java.lang.Integer::getChars (131 bytes)   inline (hot)
      @ 44   java.lang.String:: (10 bytes)   inline (hot)
        @ 1   java.lang.Object:: (1 bytes)   inline (hot)

The message callee is too large means that called method is too big to be inlined with its caller. The second appearing message, inline (hot), inlines given method directly because of its frequent use. It proves that inlining is a process depending on already mentioned parameters: size and frequent use.

The second tests consists on executing "complicated" isPair method with following arguments: -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:MaxInlineSize=30. Generated output is:

@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
// other output
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
// other output
@ 22   java.lang.Integer::intValue (5 bytes)   accessor
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   inline (hot)
  @ 1   java.lang.String::valueOf (5 bytes)   inline (hot)
    @ 1   java.lang.Integer::toString (48 bytes)   inline (hot)
      @ 24   java.lang.Integer::stringSize (21 bytes)   inline (hot)
      @ 35   java.lang.Integer::getChars (131 bytes)   inline (hot)
      @ 44   java.lang.String:: (10 bytes)   inline (hot)
        @ 1   java.lang.Object:: (1 bytes)   inline (hot)
  @ 6   java.lang.String::length (6 bytes)   inline (hot)

The output is the same as in previous case - even if isPair method is bigger than the size of method to inline (96 bytes vs 30 bytes). It's because the size of frequently called method is 325. Thus, decreasing this value to 30 (-XX:FreqInlineSize=30) makes that "complicated" isPair won't be inlined:

@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
// other output
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   callee is too large
@ 1   java.lang.String::valueOf (5 bytes)
  @ 1   java.lang.Integer::toString (48 bytes)   callee is too large
// other output
@ 28   com.waitingforcode.compilation.NotInlineTest$HardWorker::isPair (96 bytes)   too big

Naturally, too big message tells that given method is too big to be inlined - even if it's frequently used.

This post explains some points about inlining in Java. The first part describes some general information about this process. It especially informs about inline keyword in C++ that helps compiler to make right decisions about code optimization. The second part presents inlining in Java. It tells that JIT has some heurestics used to determine if given method can be "flatten". The last part tracks inlining in 2 methods: simple (short) and complicated (long). By playing with JVM arguments it shows how inlining can change.

Share on: