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

Voxxed Days 2021 - High performance with idiomatic Kotlin

44a168e6578c2cc83aaf54a38458ade9?s=47 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.

44a168e6578c2cc83aaf54a38458ade9?s=128

Magda Miu

October 22, 2021
Tweet

More Decks by Magda Miu

Other Decks in Technology

Transcript

  1. High performance with idiomatic @magdamiu

  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
  3. Table of Contents Performance is User Experience 01 Performance Bottlenecks

    02 Measure Performance Issues 03 Idiomatic Kotlin boosts Performance 04
  4. Performance is User Experience 01

  5. Objective Time vs. Subjective Time

  6. We optimize for objective time

  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.
  8. Objective Time differences of 20% or less are imperceptible. Steve

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

    MIT
  10. Performance Bottlenecks 02

  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
  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
  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
  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
  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
  16. Measure Performance Issues 03

  17. How to measure performance issues Benchmarking General performance metrics

  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.
  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
  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
  21. Profiling JVM Debugger Memory View Memory View in IntelliJ JProfiler

    Flame Graphs
  22. Idiomatic Kotlin boosts Performance 04

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

  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
  25. val evenNumbers = ArrayList<Int>() 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
  26. Double Shift Click “Decompile” Decompiled Java code

  27. Functions

  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
  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
  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
  31. inline infix fun <P> ((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
  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)
  33. None
  34. class Ref<T>(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
  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
  36. None
  37. Inline

  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") } }
  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)
  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; }
  41. inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) ->

    R?): List<T> { return sortedWith(compareBy(selector)) } Inline functions - crossinline
  42. inline fun <reified T> printTypeName() { println(T::class.simpleName) } fun main()

    { printTypeName<Int>() // Int printTypeName<Char>() // Char printTypeName<String>() // 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); }
  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?
  44. Collections

  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
  46. Collections vs Sequences Source: Collections and sequences in Kotlin by

    Florina Muntenescu
  47. // works fun List<Language>.getNames(): List<String> = this .map { it.name

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

  49. interface ValueHolder<V> { val value: V } class IntHolder :

    ValueHolder<Int> { 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
  50. interface ValueHolder<V> { val value: V } class IntHolder :

    ValueHolder<Int> { 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
  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 <T : Closeable?> Array<T>.use(block: ()->Unit) { // implementation } Disposable pattern to avoid resource leaks
  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
  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
  54. fun <T: Comparable<T>> Iterable<T>.countMin(): Int = count { it ==

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

    this.minOrNull() } fun <T: Comparable<T>> Iterable<T>.countMin(): Int { val minValue = this.minOrNull() return count { it == minValue } } Object lifting
  56. fun main(args: Array<String>) { 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
  57. Ranges

  58. fun main(args: Array<String>) { 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
  59. fun main(args: Array<String>) { 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
  60. Be curious. Start with WHY.

  61. CREDITS: This presentation template was created by Slidesgo, including icons

    by Flaticon, infographics & images by Freepik Thanks! @magdamiu magdamiu.com Slides on Twitter
  62. Resources • Benchmark ◦ kotlin-benchmarks git repo ◦ idea jmh

    plugin • Learn more about ◦ Idioms ◦ Coding conventions ◦ Clean Code with Kotlin • Inspired by ◦ Eli.wtf