Skip to content

Instantly share code, notes, and snippets.

@ForteScarlet
Forked from chrisbanes/code.kt
Created March 6, 2025 12:04
Show Gist options
  • Select an option

  • Save ForteScarlet/6969d1c39a929c99345388346f8f6f99 to your computer and use it in GitHub Desktop.

Select an option

Save ForteScarlet/6969d1c39a929c99345388346f8f6f99 to your computer and use it in GitHub Desktop.
Sequence vs List benchmark
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit
@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
class SequenceBenchmark {
@Benchmark
fun list() {
DataRepository.getItemsList()
}
@Benchmark
fun sequence() {
DataRepository.getItemsListUsingSequence()
}
@Benchmark
fun flow() {
DataRepository.getItemsListUsingFlow()
}
}
private const val ITEM_SIZE = 100
object Db {
fun getItems(): List<DbModel> {
return (0 until ITEM_SIZE).map { index ->
DbModel(
id = index,
rank = ITEM_SIZE - index, // reverse
isEnabled = index.mod(2) == 0,
)
}
}
}
object DataRepository {
fun getItemsList(): List<UiModel> {
return Db.getItems()
.filter { it.isEnabled }
.map { UiModel(it.id) }
}
fun getItemsListUsingSequence(): List<UiModel> {
return Db.getItems()
.asSequence()
.filter { it.isEnabled }
.map { UiModel(it.id) }
.toList()
}
fun getItemsListUsingFlow(): List<UiModel> = runBlocking {
Db.getItems()
.asFlow()
.filter { it.isEnabled }
.map { UiModel(it.id) }
.toList()
}
}
data class DbModel(val id: Int, val rank: Int, val isEnabled: Boolean)
data class UiModel(val id: Int)
@ForteScarlet
Copy link
Copy Markdown
Author

ForteScarlet commented Mar 6, 2025

Based on the original benchmark I got the following results (size: 100):

Benchmark                                    (size)   Mode  Cnt        Score       Error  Units
OriginalBenchmark.flow                          N/A  thrpt   10  1197880.963 ± 14766.372  ops/s
OriginalBenchmark.list                          N/A  thrpt   10  1344318.180 ± 38601.373  ops/s
OriginalBenchmark.sequence                      N/A  thrpt   10  1572128.363 ± 10080.851  ops/s

I noticed that the original benchmark included the process of building the list in the test, but it should have nothing to do with the operation we want to test.

So I took the initialisation of the list out of the benchmark and parameterised the size.
And added a couple of tests with more operations.

import kotlinx.benchmark.Benchmark
import kotlinx.benchmark.Measurement
import kotlinx.benchmark.Param
import kotlinx.benchmark.Scope
import kotlinx.benchmark.Setup
import kotlinx.benchmark.State
import kotlinx.benchmark.TearDown
import kotlinx.benchmark.Warmup
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Group
import java.util.concurrent.TimeUnit

@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
class SequenceBenchmark {

    @Param("100", "100000")
    var size = 0

    private var list: List<DbModel> = emptyList()

    @Setup
    fun setupList() {
        list = Db.getItems(size)
    }

    // Cleans up resources after each benchmark run
    @TearDown
    fun cleanup() {
        list = emptyList()
    }

    @Group("small_operation")
    @Benchmark
    fun list() {
        DataRepository.getItemsList(list)
    }

    @Group("small_operation")
    @Benchmark
    fun sequence() {
        DataRepository.getItemsListUsingSequence(list)
    }

    @Group("small_operation")
    @Benchmark
    fun flow() {
        DataRepository.getItemsListUsingFlow(list)
    }

    @Group("big_operation")
    @Benchmark
    fun list_bo() {
        DataRepository.getItemsListBigOperations(list)
    }

    @Group("big_operation")
    @Benchmark
    fun sequence_bo() {
        DataRepository.getItemsListUsingSequenceBigOperations(list)
    }

    @Group("big_operation")
    @Benchmark
    fun flow_bo() {
        DataRepository.getItemsListUsingFlowBigOperations(list)
    }
}

object Db {
    fun getItems(size: Int): List<DbModel> {
        return (0 until size).map { index ->
            DbModel(
                id = index,
                rank = size - index, // reverse
                isEnabled = index.mod(2) == 0,
            )
        }
    }
}

object DataRepository {
    fun getItemsList(items: List<DbModel>): List<UiModel> {
        return items
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
    }

    fun getItemsListBigOperations(items: List<DbModel>): List<UiModel> {
        return items
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
    }

    fun getItemsListUsingSequence(items: List<DbModel>): List<UiModel> {
        return items
            .asSequence()
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .toList()
    }

    fun getItemsListUsingSequenceBigOperations(items: List<DbModel>): List<UiModel> {
        return items
            .asSequence()
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .toList()
    }

    fun getItemsListUsingFlow(items: List<DbModel>): List<UiModel> = runBlocking {
        items
            .asFlow()
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .toList()
    }

    fun getItemsListUsingFlowBigOperations(items: List<DbModel>): List<UiModel> = runBlocking {
        items
            .asFlow()
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .filter { it.isEnabled }
            .map { UiModel(it.id, it.rank, it.isEnabled) }
            .toList()
    }
}

data class DbModel(val id: Int, val rank: Int, val isEnabled: Boolean)

data class UiModel(val id: Int, val rank: Int, val isEnabled: Boolean)

The result:

Benchmark                                    (size)   Mode  Cnt        Score        Error  Units
SequenceBenchmark.big_operation:flow_bo         100  thrpt   10   158615.807 ±   6689.494  ops/s
SequenceBenchmark.big_operation:list_bo         100  thrpt   10   309514.040 ±  10804.965  ops/s
SequenceBenchmark.big_operation:sequence_bo     100  thrpt   10   297239.770 ±   5363.794  ops/s
SequenceBenchmark.big_operation:flow_bo      100000  thrpt   10      198.088 ±      9.203  ops/s
SequenceBenchmark.big_operation:list_bo      100000  thrpt   10      248.923 ±      7.346  ops/s
SequenceBenchmark.big_operation:sequence_bo  100000  thrpt   10      304.232 ±      4.394  ops/s
SequenceBenchmark.small_operation:flow          100  thrpt   10  1343646.075 ±  57013.270  ops/s
SequenceBenchmark.small_operation:list          100  thrpt   10  1905597.088 ± 122824.654  ops/s
SequenceBenchmark.small_operation:sequence      100  thrpt   10  2333803.105 ± 146253.368  ops/s
SequenceBenchmark.small_operation:flow       100000  thrpt   10     2231.133 ±     38.463  ops/s
SequenceBenchmark.small_operation:list       100000  thrpt   10     1876.287 ±     27.674  ops/s
SequenceBenchmark.small_operation:sequence   100000  thrpt   10     2413.689 ±     48.375  ops/s

Based on the benchmarking results above, it appears to be getting a different result than the original blog.
In the case of a small number of intermediate operations and a small number of elements, a List may be slightly better than a Sequence, but in other cases, Sequence almost always prevails between the two.

@ForteScarlet
Copy link
Copy Markdown
Author

ForteScarlet commented Mar 6, 2025

1. big_operation (size=100)

xychart-beta
  title "Throughput big_operation (size=100)"
  x-axis ["Flow", "List", "Sequence"]
  y-axis "ops/s" 0 --> 350000
  bar [158616, 309514, 297240]
Loading

2. big_operation (size=100000)

xychart-beta
  title "Throughput big_operation (size=100000)"
  x-axis ["Flow", "List", "Sequence"]
  y-axis "ops/s" 0 --> 350
  bar [198, 249, 304]
Loading

3. small_operation (size=100)

xychart-beta
  title "Throughput small_operation (size=100)"
  x-axis ["Flow", "List", "Sequence"]
  y-axis "ops/s" 0 --> 2500000
  bar [1343646, 1905597, 2333803]
Loading

4. small_operation (size=100000)

xychart-beta
  title "Throughput small_operation (size=100000)"
  x-axis ["Flow", "List", "Sequence"]
  y-axis "ops/s" 0 --> 2500
  bar [2231, 1876, 2413]
Loading

汇总表格 :

Group Size Flow (ops/s) List (ops/s) Sequence (ops/s)
big_operation 100 158,616 309,514 297,240
big_operation 100000 198 249 304
small_operation 100 1,343,646 1,905,597 2,333,803
small_operation 100000 2,231 1,876 2,413

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment