Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Voxxed Days 2021 - High performance with idiomatic Kotlin

Magda Miu
October 22, 2021

Voxxed Days 2021 - High performance with idiomatic Kotlin

We develop software for people, and performance has a direct impact on the user experience.
This presentation will include details about why high performance is essential when we build software products, an overview of the most common reasons for performance issues, and how Kotlin’s features could be applied to prevent them.
The talk is for all the developers who would like to learn more about how Kotlin works under the hood and why it is a pragmatic language.
So join me at this session, and let’s discover together why Kotlin is loved by the developers and how we can write idiomatic Kotlin code to develop quality products that bring joy to our users.

Magda Miu

October 22, 2021
Tweet

More Decks by Magda Miu

Other Decks in Technology

Transcript

  1. High performance
    with idiomatic
    @magdamiu

    View Slide

  2. 40%
    of people abandon a
    website that takes
    more than 3 seconds
    to load.
    44%
    of users will tell their
    friends about a bad
    experience online
    79%
    of users who are dissatisfied
    with a website’s performance
    are less likely to buy from the
    same site again
    Source: How Loading Time Affects Your Bottom
    Line by Neil Patel

    View Slide

  3. Table of Contents
    Performance is User
    Experience
    01
    Performance
    Bottlenecks
    02
    Measure
    Performance Issues
    03
    Idiomatic Kotlin
    boosts Performance
    04

    View Slide

  4. Performance is
    User Experience
    01

    View Slide

  5. Objective Time
    vs.
    Subjective Time

    View Slide

  6. We optimize for
    objective time

    View Slide

  7. Just Noticeable
    Difference (JND)
    Any human observable
    phenomena needs to have a
    certain level of difference in order
    for that difference to be
    noticeable.

    View Slide

  8. Objective Time
    differences of 20% or less
    are imperceptible.
    Steve Seow, Microsoft

    View Slide

  9. Humans tend to
    overestimate passive
    waits by 36%.
    Richard Larson, MIT

    View Slide

  10. Performance
    Bottlenecks
    02

    View Slide

  11. Class Loader
    Method
    Area
    Heap
    Java
    Stacks
    PC
    Registers
    Native
    Method
    Stacks
    Runtime data areas
    Execution
    Engine
    Native Method
    Interface (JNI)
    Native method
    libraries (.dll,
    .so, etc.)
    JVM Architecture

    View Slide

  12. The garbage collector (GC) finds objects that are not referenced and destroys them. This principle is based
    on a set of root objects that are always reachable.
    Impacts of garbage collection: when the integrity of reference trees is difficult to be achieved the stop
    the world (GC pause) is invoked so the execution of all threads is suspended.
    Memory management
    GC Roots
    Reachable
    objects
    Non reachable
    objects => Garbage

    View Slide

  13. Heap fragmentation
    The memory fragmentation brings two challenges:
    ● Allocating space for new objects starts to be time consuming because it’s becoming hard to find the
    next free block of sufficient size
    ● The unused space between blocks of memory can become so big that the JVM won’t be able to
    create a new object
    The solution: to have a compacting step after each GC cycle. Compacting moves all reachable objects to
    one end of the heap and allocate liniary the memory.
    The impact: during this process the app is suspended.
    Fragmented Heap
    Heap after compacting

    View Slide

  14. Resource & Memory leaks
    ● A resource leak is a situation where a computer program doesn't release the resources it has
    acquired
    ○ A scarce resources that have been acquired must be released. Otherwise, an application will
    suffer from a resource leak, for example, a file handle leak
    ○ 💡 Solution provided by Kotlin: use extension function
    ● A memory leak may happen when an object can't be collected and can't be accessed by running
    code. The situation when memory that is no longer needed isn't released is referred to as a memory
    leak.
    ○ In JVM this thing can happen when a reference to an object that's no longer needed is still
    stored in another object

    View Slide

  15. ● Slow rendering is another performance issue that powerfully
    influences the user experience.
    ● The device refresh rate of a display is how many times per second
    the image on the screen can be refreshed
    ● Frame rate represents how many images software shows per
    second is the frame rate
    Slow rendering
    Update Update
    Time
    16 ms 16 ms 16 ms

    View Slide

  16. Measure
    Performance Issues
    03

    View Slide

  17. How to measure
    performance issues
    Benchmarking
    General performance
    metrics

    View Slide

  18. Benchmarking
    Microbenchmarks
    These are metrics showing the performance of certain functions. Use Java Microbenchmark Harness (JMH)
    plugin. There is a dedicated repo with Kotlin benchmarks done by JetBrains.
    Macrobenchmarks
    These are the opposite of microbenchmarks; test the entire application.
    Mesobenchmarks
    These are something in-between, measuring features or workflows.

    View Slide

  19. General performance metrics
    Benchmarking is a small part of performance testing. The main focus of performance testing is checking
    software:
    ● Speed: To determine how fast the application responds
    ● Scalability: To determine the maximum number of users that an application can handle
    ● Stability: To determine how the application invokes its functions under different loads

    View Slide

  20. Performance testing
    ● Benchmark testing (we're already familiar with this)
    ● Load testing, which determines how an application works under anticipated user loads
    ● Volume testing, which tests what happens when a large amount of data populates a database
    ● Scalability testing, which determines how an application invokes functions with a large number of
    users
    ● Stress testing, which involves testing an application under extreme workloads
    ● Tools to analyse the results of performance testing: JMeter, Firebase Crashlytics, Dynatrace,
    Grafana & Prometheus

    View Slide

  21. Profiling
    JVM Debugger Memory View Memory View in IntelliJ JProfiler
    Flame Graphs

    View Slide

  22. Idiomatic Kotlin
    boosts Performance
    04

    View Slide

  23. Programming Paradigms
    Declarative Imperative
    Programming
    Logic Functional Object-oriented Procedural

    View Slide

  24. Functional Programming
    Key Concepts
    ● First-class functions
    ● Immutability
    ● No side effects
    ● Function types
    ● Lambda expressions
    ● Data classes
    ● A rich set of APIs for working with
    objects and collections in a
    functional style
    Functional Style in Kotlin

    View Slide

  25. val evenNumbers = ArrayList()
    for(index in 0..numbers.lastIndex) {
    val currentNumber = numbers[index]
    if(currentNumber % 2 == 0) {
    evenNumbers.add(currentNumber)
    }
    }
    val evenNumbers = numbers.filter { it % 2 == 0 }
    Imperative style
    Declarative style

    View Slide

  26. Double Shift Click “Decompile” Decompiled Java code

    View Slide

  27. Functions

    View Slide

  28. class Book(val title: String, val author: String) {
    // impure function
    fun getBookDetails() = "$title - $author"
    }
    // pure function
    fun getBookDetails(title: String, author: String) = "$title - $author"
    Pure functions

    View Slide

  29. fun titleStartsWithS(book: Book) = book.title.startsWith("S")
    fun lengthOfTitleGraterThan5(book: Book) = book.title.length > 5
    fun authorStartsWithB(book: Book) = book.author.startsWith("B")
    val book1 = Book("Start with why", "Simon Sinek")
    val book2 = Book("Dare to lead", "Brene Brown")
    val books = listOf(book1, book2)
    val filteredBooks = books
    .filter(::titleStartsWithS)
    .filter(::authorStartsWithB)
    .filter(::lengthOfTitleGraterThan5)
    High-order functions

    View Slide

  30. // dedicated predicat
    fun allFilters(book: Book): Boolean = titleStartsWithS(book)
    && lengthOfTitleGraterThan5(book)
    && authorStartsWithB(book)
    // anonymous function
    books.filter(fun(book: Book) = titleStartsWithS(book)
    && lengthOfTitleGraterThan5(book)
    && authorStartsWithB(book))
    1st and 2nd possible solutions

    View Slide

  31. inline infix fun ((P) -> Boolean).and(crossinline predicate: (P) -> Boolean): (P) -> Boolean {
    return { p: P -> this(p) && predicate(p) }
    }
    books.filter(
    ::titleStartsWithS
    and ::authorStartsWithB
    and ::lengthOfTitleGraterThan5
    )
    3rd possible solution: function composition

    View Slide

  32. println(books.minByOrNull { it.year })
    var priceBooks = 0.0
    val prefixPriceDetails = "The current sum is "
    books.forEach {
    priceBooks += it.price
    println("$prefixPriceDetails $priceBooks")
    }
    Lambdas expressions & Closure (capturing lambda)

    View Slide

  33. View Slide

  34. class Ref(var value: T)
    fun main() {
    val counter = Ref(0)
    val incrementAction = { counter.value++ }
    }
    Class used to simulate capturing a mutable
    variable
    An immutable variable is captured, but the
    actual value is stored in a field and can be
    changed
    Capturing a mutable variable

    View Slide

  35. public static final void main() {
    final IntRef counter = new IntRef();
    counter.element = 0;
    Function0 incrementAction = (Function0)(new Function0() {
    // $FF: synthetic method
    // $FF: bridge method
    public Object invoke() {
    return this.invoke();
    }
    public final int invoke() {
    IntRef var10000 = counter;
    int var1;
    var10000.element = (var1 = var10000.element) + 1;
    return var1;
    }
    });
    incrementAction.invoke();
    }
    fun main() {
    var counter = 0;
    val incrementAction = {
    counter++
    }
    incrementAction()
    }
    Decompiled Java code

    View Slide

  36. View Slide

  37. Inline

    View Slide

  38. public static final void operation(@NotNull Function0 op) {
    Intrinsics.checkNotNullParameter(op, "op");
    String var1 = "Before calling op()";
    boolean var2 = false;
    System.out.println(var1);
    op.invoke();
    var1 = "After calling op()";
    var2 = false;
    System.out.println(var1);
    }
    public static final void main() {
    operation((Function0)null.INSTANCE);
    }
    // $FF: synthetic method
    public static void main(String[] var0) {
    main();
    }
    fun operation(op: () -> Unit) {
    println("Before calling op()")
    op()
    println("After calling op()")
    }
    fun main() {
    operation { println("This is the
    actual op function") }
    }

    View Slide

  39. public static final void operation(@NotNull Function0 op) {
    Intrinsics.checkNotNullParameter(op, "op");
    String var1 = "Before calling op()";
    boolean var2 = false;
    System.out.println(var1);
    op.invoke();
    var1 = "After calling op()";
    var2 = false;
    System.out.println(var1);
    }
    public static final void main() {
    operation((Function0)null.INSTANCE);
    }
    // $FF: synthetic method
    public static void main(String[] var0) {
    main();
    }
    inline fun operation(op: () -> Unit) {
    println("Before calling op()")
    op()
    println("After calling op()")
    }
    fun main() {
    operation { println("This is the
    actual op function") }
    }
    The operation function code will be
    copied here (bytecode level)

    View Slide

  40. inline fun computeValues(
    number: Int, doubleValue: (number: Int) ->
    Unit,
    noinline tripleValue: (number: Int) -> Unit
    ) {
    doubleValue.invoke(number)
    tripleValue.invoke(number)
    }
    fun main() {
    val number = 7;
    computeValues(number,
    { println(doubleOfNumber(number)) },
    { println(tripleOfNumber(number)) })
    }
    fun doubleOfNumber(number: Int) = 2 * number
    fun tripleOfNumber(number: Int) = 3 * number
    public static final void computeValues(int number, @NotNull Function1 doubleValue, @NotNull Function1 tripleValue) {
    int $i$f$computeValues = 0;
    Intrinsics.checkNotNullParameter(doubleValue, "doubleValue");
    Intrinsics.checkNotNullParameter(tripleValue, "tripleValue");
    doubleValue.invoke(number);
    tripleValue.invoke(number);
    }
    public static final void main() {
    final int number = 7;
    Function1 tripleValue$iv = (Function1)(new Function1() {
    // $FF: synthetic method
    // $FF: bridge method
    public Object invoke(Object var1) {
    this.invoke(((Number)var1).intValue());
    return Unit.INSTANCE;
    }
    public final void invoke(int it) {
    int var2 = NoinlineSamplesKt.tripleOfNumber(number);
    boolean var3 = false;
    System.out.println(var2);
    }
    });
    int $i$f$computeValues = false;
    int var4 = false;
    int var5 = doubleOfNumber(number);
    boolean var6 = false;
    System.out.println(var5);
    tripleValue$iv.invoke(Integer.valueOf(number));
    }
    // $FF: synthetic method
    public static void main(String[] var0) {
    main();
    }
    public static final int doubleOfNumber(int number) {
    return 2 * number;
    }
    public static final int tripleOfNumber(int number) {
    return 3 * number;
    }

    View Slide

  41. inline fun > Iterable.sortedBy(crossinline selector: (T) -> R?): List {
    return sortedWith(compareBy(selector))
    }
    Inline functions - crossinline

    View Slide

  42. inline fun printTypeName() {
    println(T::class.simpleName)
    }
    fun main() {
    printTypeName() // Int
    printTypeName() // Char
    printTypeName() // String
    println(Int::class.simpleName) // Int
    println(Char::class.simpleName) // Char
    println(String::class.simpleName) // String
    }
    Reified types
    public static final void main() {
    int $i$f$printTypeName = false;
    String var1 =
    Reflection.getOrCreateKotlinClass(Integer.class).getSimpleName();
    boolean var2 = false;
    System.out.println(var1);
    $i$f$printTypeName = false;
    var1 =
    Reflection.getOrCreateKotlinClass(Character.class).getSimpleName();
    var2 = false;
    System.out.println(var1);
    $i$f$printTypeName = false;
    var1 = Reflection.getOrCreateKotlinClass(String.class).getSimpleName()
    var2 = false;
    System.out.println(var1);
    String var3 =
    Reflection.getOrCreateKotlinClass(Integer.TYPE).getSimpleName();
    boolean var4 = false;
    System.out.println(var3);
    var3 =
    Reflection.getOrCreateKotlinClass(Character.TYPE).getSimpleName();
    var4 = false;
    System.out.println(var3);
    var3 = Reflection.getOrCreateKotlinClass(String.class).getSimpleName()
    var4 = false;
    System.out.println(var3);
    }

    View Slide

  43. fun main() {
    var counter = 0
    repeat(3000) {
    counter++
    }
    repeat3000 {
    counter++
    }
    println(counter)
    }
    fun repeat3000(action: (Int) -> Unit) {
    for (index in 0..2999) {
    action(index)
    }
    }
    public static final void main() {
    final IntRef counter = new IntRef();
    counter.element = 0;
    short var1 = 3000;
    boolean var2 = false;
    boolean var3 = false;
    int var9 = 0;
    for(short var4 = var1; var9 < var4; ++var9) {
    int var6 = false;
    int var10001 = counter.element++;
    }
    repeat3000((Function1)(new Function1() {
    // $FF: synthetic method
    // $FF: bridge method
    public Object invoke(Object var1) {
    this.invoke(((Number)var1).intValue());
    return Unit.INSTANCE;
    }
    public final void invoke(int it) {
    int var10001 = counter.element++;
    }
    }));
    int var8 = counter.element;
    var2 = false;
    System.out.println(var8);
    }
    // $FF: synthetic method
    public static void main(String[] var0) {
    main();
    }
    public static final void repeat3000(@NotNull Function1 action) {
    Intrinsics.checkNotNullParameter(action, "action");
    int index = 0;
    for(short var2 = 2999; index <= var2; ++index) {
    action.invoke(index);
    }
    }
    Is there any performance difference?

    View Slide

  44. Collections

    View Slide

  45. fun main() {
    measure { smallList() } // 11ms*
    measure { smallSequence() } // 6ms*
    }
    fun smallList() = (0..5)
    .filter { print("list filter($it) "); it % 2 == 0 }
    .map { print("list map($it) "); it * it }
    .first()
    fun smallSequence() = (0..5)
    .asSequence()
    .filter { print("seq filter($it) "); it % 2 == 0 }
    .map { print("seq map($it) "); it * it }
    .first()
    // output
    list filter(0) list filter(1) list filter(2) list filter(3) list filter(4) list filter(5) list map(0) list map(2) list map(4) 11 ms
    seq filter(0) seq map(0) 6 ms
    sequence.filter{...}.map{...}.first()
    intermediate operation
    terminal operation
    Collections vs Sequences

    View Slide

  46. Collections vs Sequences
    Source: Collections and sequences in Kotlin by
    Florina Muntenescu

    View Slide

  47. // works
    fun List.getNames(): List = this
    .map { it.name }
    .filter { it != null }
    .map { it!! }
    // better
    fun List.getNames(): List = this
    .map { it.name }
    .filterNotNull()
    // best
    fun List.getNames(): List = this
    .mapNotNull { it.name }
    Collections: limit the number of operations // filter Collections
    public inline fun Iterable.filter(predicate: (T
    -> Boolean): List {
    return filterTo(ArrayList(), predicate)
    }
    // map Collections
    public inline fun Iterable.map(transform: (T
    -> R): List {
    return
    mapTo(ArrayList(collectionSizeOrDefault(10)),
    transform)
    }
    // map Sequence
    public fun Sequence.map(transform: (T) -> R)
    Sequence {
    return TransformingSequence(this, transform)
    }

    View Slide

  48. Best Practices

    View Slide

  49. interface ValueHolder {
    val value: V
    }
    class IntHolder : ValueHolder {
    override val value: Int
    get() = Random().nextInt()
    }
    fun main() {
    val sample = IntHolder()
    println(sample.value) //260078462
    println(sample.value) //1657381068
    }
    // immutability by default
    data class ImmutableKey(val name: String? = null)
    Immutability

    View Slide

  50. interface ValueHolder {
    val value: V
    }
    class IntHolder : ValueHolder {
    override val value: Int
    get() = Random().nextInt()
    }
    fun main() {
    val sample = IntHolder()
    println(sample.value) //260078462
    println(sample.value) //1657381068
    }
    // immutability by default
    data class ImmutableKey(val name: String? = null)
    Immutability

    View Slide

  51. inputStream.use {
    outputStream.use {
    // do something with the streams
    outputStream.write(inputStream.read())
    }
    }
    // improved option
    arrayOf(inputStream, outputStream).use {
    // do something with the streams
    outputStream.write(inputStream.read())
    }
    // use implementation
    private inline fun Array.use(block: ()->Unit) {
    // implementation
    }
    Disposable pattern to avoid resource leaks

    View Slide

  52. val displayContent = "Hello " + firstName + "! Please confirm that " + email + " is your email address."
    val displayContentWithTemplate = "Hello $firstName! Please confirm that $email is your email address."
    StringBuilder / dynamic invocations on JVM 9+ targets

    View Slide

  53. class Location(@JvmField var latitude: Double,
    @JvmField var longitude: Double)
    public final class Location {
    @JvmField
    public double latitude;
    @JvmField
    public double longitude;
    public Location(double latitude, double longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
    }
    }
    @JvmField

    View Slide

  54. fun > Iterable.countMin(): Int =
    count { it == this.minOrNull() }
    fun > Iterable.countMin(): Int {
    val minValue = this.minOrNull()
    return count { it == minValue }
    }
    Object lifting

    View Slide

  55. fun > Iterable.countMin(): Int =
    count { it == this.minOrNull() }
    fun > Iterable.countMin(): Int {
    val minValue = this.minOrNull()
    return count { it == minValue }
    }
    Object lifting

    View Slide

  56. fun main(args: Array) {
    val value = args[0].toIntOrNull()
    if (value in 0..10) {
    println(value)
    }
    }
    public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    Integer value = StringsKt.toIntOrNull(args[0]);
    byte var2 = 0;
    if (CollectionsKt.contains((Iterable)(new
    IntRange(var2, 10)), value)) {
    System.out.println(value);
    }
    }
    Ranges

    View Slide

  57. Ranges

    View Slide

  58. fun main(args: Array) {
    val range = 0..10
    val input = 7
    if (input in range) {
    println(input)
    }
    }
    public static final void main(@NotNull String[] args) {
    Intrinsics.checkNotNullParameter(args, "args");
    byte input = 0;
    IntRange range = new IntRange(input, 10);
    input = 7;
    if (range.contains(input)) {
    boolean var3 = false;
    System.out.println(input);
    }
    }
    Ranges

    View Slide

  59. fun main(args: Array) {
    val input = args[0].toInt()
    if (input in 0..10) {
    println(input)
    }
    }
    public static final void main(@NotNull String[] args) {
    Intrinsics.checkNotNullParameter(args, "args");
    String var2 = args[0];
    boolean var3 = false;
    int input = Integer.parseInt(var2);
    if (0 <= input) {
    if (10 >= input) {
    boolean var4 = false;
    System.out.println(input);
    }
    }
    }
    Ranges

    View Slide

  60. Be curious.
    Start with WHY.

    View Slide

  61. CREDITS: This presentation template was created by Slidesgo, including icons by Flaticon, infographics & images by Freepik
    Thanks!
    @magdamiu
    magdamiu.com
    Slides on Twitter

    View Slide

  62. Resources
    ● Benchmark
    ○ kotlin-benchmarks git repo
    ○ idea jmh plugin
    ● Learn more about
    ○ Idioms
    ○ Coding conventions
    ○ Clean Code with Kotlin
    ● Inspired by
    ○ Eli.wtf

    View Slide