Skip to content

Commit

Permalink
inline functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Andreas Haindl committed Mar 19, 2024
1 parent 7170201 commit c5caccc
Show file tree
Hide file tree
Showing 15 changed files with 411 additions and 0 deletions.
1 change: 1 addition & 0 deletions Coroutines/Async vs Launch/AsyncTask1/task-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type: edu
17 changes: 17 additions & 0 deletions Inline/Inline Functions/Closure/src/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import kotlin.random.Random

fun main() {

val counter = createCounter { Random.nextInt(10) }

println(counter())
println(counter())
println(counter())
}

inline fun createCounter(numberCreator: () -> Int): () -> Int {
var count = numberCreator()
return {
count++
}
}
16 changes: 16 additions & 0 deletions Inline/Inline Functions/Closure/task-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type: choice
is_multiple_choice: true
options:
- text: A developer can achieve optimization using the inline keyword as a silver bullet.
is_correct: false
- text: Closure captures the references to variables of the enclosing scope of a lambda.
is_correct: true
- text: Closure captures the value to variables of the enclosing scope of a lambda.
is_correct: false
- text: Closure is a concept with great benefits only.
is_correct: false
- text: Using the inline keyword thoughtlessly may introduce negative effects.
is_correct: true
files:
- name: src/Main.kt
visible: true
142 changes: 142 additions & 0 deletions Inline/Inline Functions/Closure/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Closure

Lambdas can capture and have access to their surrounding scope, the closure.
The closure needs to maintain the state of the variables.
This is true even after the function has finished executing.

Hence, closures introduce a runtime overhead because additional references must be maintained leading to a memory overhead.

## Facts

### Closure Creation

When a higher-order function creates a closure, it captures references to the variables from its enclosing scope.
This involves storing these references within the closure object itself.

### Closure Lifetime

The closure is still alive even after the enclosing function has completed execution.
The closure may still hold references to variables from the enclosing scope.

### Garbage Collection

Closure retains references to variables from the enclosing scope.
Those variables cannot be garbage-collected as long as the closure exists.

### Additional Objects

The closure itself may introduce additional objects or overhead.
Like
* compiler-generated class to hold the captured variables
* additional metadata needed to support closures in the runtime environment.

## Decompiled Example

```java
public static final void main() {
Function0 counter = createCounter((Function0)null.INSTANCE);
int var1 = ((Number)counter.invoke()).intValue();
System.out.println(var1);
var1 = ((Number)counter.invoke()).intValue();
System.out.println(var1);
var1 = ((Number)counter.invoke()).intValue();
System.out.println(var1);
}

@NotNull
public static final Function0 createCounter(@NotNull Function0 numberCreator) {
Intrinsics.checkNotNullParameter(numberCreator, "numberCreator");
final Ref.IntRef count = new Ref.IntRef();
count.element = ((Number)numberCreator.invoke()).intValue();
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final int invoke() {
Ref.IntRef var10000 = count;
int var1;
var10000.element = (var1 = var10000.element) + 1;
return var1;
}
});
}
```

## Decompiled with inline keyword

```java
public final class MainKt {
public static final void main() {
int $i$f$createCounter = false;
Ref.IntRef count$iv = new Ref.IntRef();
int var3 = false;
int var5 = Random.Default.nextInt(10);
count$iv.element = var5;
Function0 counter = (Function0) (new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final int invoke() {
Ref.IntRef var10000 = count;
int var1;
var10000.element = (var1 = var10000.element) + 1;
return var1;
}
});
int var6 = ((Number) counter.invoke()).intValue();
System.out.println(var6);
var6 = ((Number) counter.invoke()).intValue();
System.out.println(var6);
var6 = ((Number) counter.invoke()).intValue();
System.out.println(var6);
}

@NotNull
public static final Function0 createCounter(@NotNull Function0 numberCreator) {
int $i$f$createCounter = 0;
Intrinsics.checkNotNullParameter(numberCreator, "numberCreator");
final Ref.IntRef count = new Ref.IntRef();
count.element = ((Number)numberCreator.invoke()).intValue();
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final int invoke() {
Ref.IntRef var10000 = count;
int var1;
var10000.element = (var1 = var10000.element) + 1;
return var1;
}
});
}
}
```

## Observation

The `inline` keyword doesn't offer any significant benefits.
Inlining a function that returns a lambda with captured variables doesn't get inlined in the call site.
The closure capturing still happens.

The decompiled code has an overhead.
The lambda function to return is compiled twice.
* In the main method as the `counter` function due to the inlining of the `createCounter` higher-order function.
* As separate function `createCounter` which is not used

## Conclusion

The `inline` keyword is no silver bullet for performance optimizations.
Used in the wrong context it will not gain any signification optimizations.
Even worse it will create some code bloat introducing a negative effect.

> It is essential to use keyword with care in the right context to gain benefits.
9 changes: 9 additions & 0 deletions Inline/Inline Functions/Introduction/src/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fun main() {
highOrderFunction {
println(it)
}
}

fun highOrderFunction(lambda: (String) -> Unit) {
lambda("Hello World")
}
14 changes: 14 additions & 0 deletions Inline/Inline Functions/Introduction/task-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type: choice
is_multiple_choice: true
options:
- text: Kotlins functions are first-class citizens which can take other functions as parameters.
is_correct: true
- text: These functions are called higher-order functions.
is_correct: true
- text: The call site is place where the higher-order function is invoked.
is_correct: true
- text: The inline keyword has an impact on the compilation of high-order functions.
is_correct: true
files:
- name: src/Main.kt
visible: true
113 changes: 113 additions & 0 deletions Inline/Inline Functions/Introduction/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Inline Functions

Kotlin offers first class functions.
These let you store functions in variables, pass them as arguments to and return them from other higher-order functions.

As each function is an object, creating higher-order functions leads to a new object creation and memory allocation.

Inline Functions are a powerful feature to improve performance and reduce overhead of higher-order functions.
Marking a function with the `inline` keyword, the compiler replaces every call to that function with the actual function body.
Hence, the function body is inlined at the call site and object creation is avoided.

## Benefits

### Function Body Replacement

Using `inline` on a function the compiler replaces every call to that function with the actual function body of the lambda.
This avoids the overhead of a function call.

### Context Preservation

Inline functions can access variables from the surrounding scope including private variables.
This possible due to the fact that the code is essentially copied into the call site, so it has access to all the variables available at that location.

### Improves Performance

By eliminating the overhead of function calls, inline functions can improve the performance of your code.
Especially in scenarios calling small functions frequently within loops.

## Example

### Simple Function
```kotlin
// Declaration
fun highOrderFunction(lambda: (String) -> Unit) {
lambda("Hello World")
}

// Call
highOrderFunction {
println(it)
}
```

#### Decompiled

> For sake of simplicity the meta information are omitted
```java
public final class MainKt {
public static final void main() {
highOrderFunction((Function1)null.INSTANCE);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}

public static final void highOrderFunction(@NotNull Function1 lambda) {
Intrinsics.checkNotNullParameter(lambda, "lambda");
lambda.invoke("Hello World");
}
}
```

* There is an instantiation of the lambda -> Function1
* There is a call to `invoke` to trigger the lambda

### As Inline Function

```kotlin
// Declaration
inline fun highOrderFunction(lambda: (String) -> Unit) {
lambda("Hello World")
}

// Call
highOrderFunction {
println(it)
}
```

#### Decompiled

> For sake of simplicity the meta information are omitted
```java
public final class MainKt {
public static final void main() {
int $i$f$highOrderFunction = false;
String it = "Hello World";
int var2 = false;
System.out.println(it);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}

public static final void highOrderFunction(@NotNull Function1 lambda) {
int $i$f$highOrderFunction = 0;
Intrinsics.checkNotNullParameter(lambda, "lambda");
lambda.invoke("Hello World");
}
}
```

* The `higherOrderFunction` is compiled but not called
* The lambda passed to the function is inlined in the `main()`
* The `println` statement is inlined
* No instantiation of the higher-order function
* No invocation of the lambda
19 changes: 19 additions & 0 deletions Inline/Inline Functions/Visibility/src/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Wont compile

//fun main() {
// val main = Main()
//
// main.publicInlineFun { main.privateFun() }
//}
//
//class Main {
//
// private fun privateFun(): Int {
// return 42
// }
//
// inline fun publicInlineFun(lambda: () -> Unit) {
// lambda()
// privateFun()
// }
//}
12 changes: 12 additions & 0 deletions Inline/Inline Functions/Visibility/task-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type: choice
is_multiple_choice: true
options:
- text: Inline functions have access to all members at any time.
is_correct: false
- text: The @PublishedApi annotations purpose is to control the member visibility of classes.
is_correct: false
- text: Inline functions can be private.
is_correct: true
files:
- name: src/Main.kt
visible: true
19 changes: 19 additions & 0 deletions Inline/Inline Functions/Visibility/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Visibility

Inline functions are restricted to be public.
This is because of how inlining works and the complications it introduces to the visibility of the functions code on the call site.

The body of an inline function gets copied directly at the call site during compilation.
If the function were marked as `private`, then the inlined code would only be accessible within the same file where the function is defined.

However, inlining effectively breaks this file-level encapsulation because the inlined code is copied into other files where the function is called.
This would mean that the inlined code could potentially be accessible outside the file where the private function is defined, violating encapsulation.

To prevent this from happening and to maintain the encapsulation provided by private visibility, Kotlin does not allow private functions to be marked as inline.
Instead, if you need to inline a function for performance reasons, you can consider using internal visibility, which allows the function to be accessed from within the same module but not outside of it.

## @PublishedApi

The `@PublishedApi` annotation is used to mark declarations that are intended to be part of the public API of a module, but are not meant to be exposed to consumers of that module.
This annotation is often used in conjunction with inline functions to control the visibility.
Marked functions are used internally within the module but should not be exposed externally.
5 changes: 5 additions & 0 deletions Inline/Inline Functions/lesson-info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
content:
- Introduction
- Closure
- noinline keyword
- Visibility
20 changes: 20 additions & 0 deletions Inline/Inline Functions/noinline keyword/src/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
fun main() {
Main().highOrderFunction {
println(it)
}
}

class Main {
fun highOrderFunction(lambda: (String) -> Unit) {
val value = "value"
lambda("Hello World")
}

private fun privateFunction() {}

inline fun shinyFunction(inlineLambda: (String) -> Unit, noinline noInlineLambda: (String) -> Unit) {
val x = noInlineLambda
highOrderFunction { noInlineLambda("") }
}
}

Loading

0 comments on commit c5caccc

Please sign in to comment.