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

We should stop using List everywhere

Avatar for Rafał Sumisławski Rafał Sumisławski
February 18, 2022
660

We should stop using List everywhere

Avatar for Rafał Sumisławski

Rafał Sumisławski

February 18, 2022

Transcript

  1. About me - Development Team Leader at - Co-maintainer of

    - @sumislawski on 2/30 Rafał Sumisławski
  2. Adam 5/30 case class Batch(devices: Seq[Device] = Seq()) { def

    addDevice(d: Device): Batch = copy(devices = devices :+ d) } Tom 3km (1.86m i) “Eagle Eye”
  3. 6/30 case class Batch(devices: Seq[Device] = Seq()) { def addDevice(d:

    Device): Batch = copy(devices = devices :+ d) } Seq() === List()
  4. 7/30 case class Batch(devices: Seq[Device] = Seq()) { def addDevice(d:

    Device): Batch = copy(devices = devices :+ d) } devices :+ d Copy the whole thing. Super slow. d +: devices Structural sharing. Fast.
  5. 8/30 case class Batch(devices: Seq[Device] = Seq()) { def addDevice(d:

    Device): Batch = copy(devices = devices :+ d) } devices: Seq[Device] = ???
  6. List and alternatives In scope: - List - cats.data.Chain -

    ArraySeq / Array + ArrayOps - Vector 10/30 Out of scope: - Queue - Stack - SeqView - Stream - LazyList - String
  7. Assumptions / terminology Assuming: 64-bit HotSpot JVM with default settings

    (<=32GB) • 12 bytes object headers • 4 bytes references Memory Overhead - excluding object size and reference 11/30
  8. List 13/30 @tailrec def sum(l: List[Int], acc: Int = 0):

    Int = l match { case head :: tail => sum(tail, acc + head) case Nil => acc }
  9. List Strengths: - Perfect head/tail/prepend performance - Great for iterating

    with a recursive function - Simple - The most efficient single element Seq Weaknesses: - O(n) size and append operations - Huge memory overheads (20 bytes/element) - Cache/GC unfriendly memory layout - map allocates n new objects 14/30
  10. Chain Strengths: - Can wrap and concatenate other collections in

    O(1) - Cheap append and prepend Weaknesses: - Doesn’t implement Seq - Unpredictable performance due to wrapping - High memory overheads without wrapping 16/30
  11. ArraySeq Strengths: - Cache and GC friendly memory layout -

    Optimal performance of map, filter, etc. - Specialisations for primitive types - Can wrap an Array - Low memory overheads (~0 bytes / element) Weaknesses: - Modifying operations copy the whole structure - map kills specialisation 17/30
  12. Array (ArrayOps) Strengths: - <Like ArraySeq> - map doesn’t break

    specialisation - One object instead of two Weaknesses: - <Like ArraySeq> - Doesn’t implement Seq - equals / hashCode - Is mutable 18/30
  13. Vector 19/30 w w w In reality: w = 32

    My illustrations: w = 4 Vector3 Vector0 Vector1 Vector2 w w w
  14. Vector 21/30 prefix2 data3 suffix2 <= w - 2 prefix1

    prefix2 data3 suffix2 suffix1 <= w - 1 <= w - 2 <= w
  15. Vector 22/30 prefix1 prefix2 data3 w suffix2 Vector3 (full from

    both sides) prefix1 prefix2 data3 suffix1 suffix2 Vector3 suffix1 w w W = 32 <= w - 1 <= w - 2 <= w
  16. prepending to Vector 23/30 prefix1 data2 suffix1 Vector2 prefix1 data2

    suffix1 Vector2 prefix1 data2 suffix1 Vector2 prefix1 data2 suffix1 Vector2
  17. prepending to Vector 24/30 prefix1 data2 suffix1 Vector2 prefix1 prefix2

    data3 suffix1 suffix2 Vector3 prefix1 prefix2 data3 suffix1 suffix2 Vector3 prefix1 prefix2 data3 suffix1 suffix2 Vector3
  18. Vector Strengths: - Cache and GC friendly memory layout -

    Good map/filter/… performance - Low memory overheads (~0.8 bytes / element) - Moderate amount of allocations - Good performance in most scenarios Weaknesses: - primitive types need to be wrapped - constructing Vector element-by-element is slower (than a List) 25/30
  19. Collection performance map 26/30 map prepend append concatenate head last

    tail init size memory overhead List 87237 874060451 76367 86192 3014116681 890417 10509606340 84359 882075high Chain 103260 517983073 521372484 869954402 79774408 114322318 110757063 110048556 546774it depends ArraySeq 423038 487513 491109 259590 2661878593 2632565383 542815 541630 3076116208 optimised primitives* Array 460701 470650 472752 255946 2913218119 2847119273 520431 520956 3471757103 optimised primitives Vector 345023 154617988 155631251 433548 2845001516 2601371242 155897483 156617315 3311628378low bad good awesome Numbers are operations per second Collection size = 10000
  20. Why do we use List everywhere? - List are common

    in teaching - Immutability and structural sharing - Recursion - Standard library uses List as default Seq - Cats prefer List (e.g. Reducable#toList, Applicative#replicateA) - Because Haskel? 27/30
  21. Why should we stop? - Tricky performance characteristics - Huge

    memory overheads - Bad memory layout - There is a better alternative 28/30
  22. What should we do? 1. Use Vector by default 2.

    Use List, ArraySeq, ..., where their advantage is obvious 3. Specify precise collection type Use Seq only if needed and without impact on performance 4. Monitor performance in production. Profile hot paths. Optimise accordingly. 29/30