web-dev-qa-db-fra.com

Kotlin: Mettre à jour un élément de la liste immuable

Kotlin débutant ici. Comment prendre une liste et sans la transformer, créer une deuxième liste (immuable) avec un élément mis à jour à un index spécifique?

Je pense à deux manières, qui semblent toutes deux impliquer des succès en performance, une mutation de l'objet sous-jacent, ou les deux.

data class Player(val name: String, val score: Int = 0)

val players: List<Player> = ...

// Do I do this?
val updatedPlayers1 = players.mapIndexed { i, player ->
    if (i == 2) player.copy(score = 100)
    else player
}

// Or this?
val updatedPlayer = players[2].copy(score = 100)
val mutable = players.toMutableList()
mutable.set(2, updatedPlayer)
val updatedPlayers2 = mutable.toList()

S'il n'existe aucun moyen performant de le faire, existe-t-il une structure de données plus appropriée dans Kotlin stdlib ou une autre bibliothèque? Kotlin ne semble pas avoir de vecteurs.

9
Eric

Pour moi, évident que cette deuxième voie devrait être plus rapide, mais combien?

J'ai donc écrit quelques repères ici

@State(Scope.Thread)
open class ModifyingImmutableList {

    @Param("10", "100", "10000", "1000000")
    var size: Int = 0

    lateinit var players: List<Player>

    @Setup
    fun setup() {
        players = generatePlayers(size)
    }

    @Benchmark fun iterative(): List<Player> {
        return players.mapIndexed { i, player ->
            if (i == 2) player.copy(score = 100)
            else player
        }
    }

    @Benchmark fun toMutable(): List<Player> {
        val updatedPlayer = players[2].copy(score = 100)
        val mutable = players.toMutableList()
        mutable.set(2, updatedPlayer)
        return mutable.toList()
    }

    @Benchmark fun toArrayList(): List<Player> {
        val updatedPlayer = players[2].copy(score = 100)
        return players.set(2, updatedPlayer)
    }
}

Et obtenu suivant résultats :

$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark                            (size)   Mode  Cnt         Score        Error  Units
ModifyingImmutableList.iterative         10  thrpt  100   6885018.769 ± 189148.764  ops/s
ModifyingImmutableList.iterative        100  thrpt  100    877403.066 ±  20792.117  ops/s
ModifyingImmutableList.iterative      10000  thrpt  100     10456.272 ±    382.177  ops/s
ModifyingImmutableList.iterative    1000000  thrpt  100       108.167 ±      3.506  ops/s
ModifyingImmutableList.toArrayList       10  thrpt  100  33278431.127 ± 560577.516  ops/s
ModifyingImmutableList.toArrayList      100  thrpt  100  11009646.095 ± 180549.177  ops/s
ModifyingImmutableList.toArrayList    10000  thrpt  100    129167.033 ±   2532.945  ops/s
ModifyingImmutableList.toArrayList  1000000  thrpt  100       528.502 ±     16.451  ops/s
ModifyingImmutableList.toMutable         10  thrpt  100  19679357.039 ± 338925.701  ops/s
ModifyingImmutableList.toMutable        100  thrpt  100   5504388.388 ± 102757.671  ops/s
ModifyingImmutableList.toMutable      10000  thrpt  100     62809.131 ±   1070.111  ops/s
ModifyingImmutableList.toMutable    1000000  thrpt  100       258.013 ±      8.076  ops/s

Donc, ces tests montrent que parcourir plus de collection environ 3 à 6 fois plus lentement que la copie. Aussi, je fournis mon implémentation: toArray , qui ressemble à plus performant.

Sur 10 éléments, la méthode toArray a un débit 33278431.127 ± 560577.516 opérations par seconde. Est-ce lent? Ou c'est extrêmement rapide? J'écris le test "de base", qui montre le coût de la copie de Players et du tableau en mutation. Résultats intéressants:

@Benchmark fun baseline(): List<Player> {
    val updatedPlayer = players[2].copy(score = 100)
    mutable[2] = updatedPlayer;
    return mutable
}

Où mutable - juste MutableList, qui est ArrayList

$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark                            (size)   Mode  Cnt         Score         Error  Units
ModifyingImmutableList.baseline          10  thrpt  100  81026110.043 ± 1076989.958  ops/s
ModifyingImmutableList.baseline         100  thrpt  100  81299168.496 ±  910200.124  ops/s
ModifyingImmutableList.baseline       10000  thrpt  100  81854190.779 ± 1010264.620  ops/s
ModifyingImmutableList.baseline     1000000  thrpt  100  83906022.547 ±  615205.008  ops/s
ModifyingImmutableList.toArrayList       10  thrpt  100  33090236.757 ±  518459.863  ops/s
ModifyingImmutableList.toArrayList      100  thrpt  100  11074338.763 ±  138272.711  ops/s
ModifyingImmutableList.toArrayList    10000  thrpt  100    131486.634 ±    1188.045  ops/s
ModifyingImmutableList.toArrayList  1000000  thrpt  100       531.425 ±      18.513  ops/s

Nous avons une régression 2x, et sur 1 million environ 150000x!

Donc, ArrayList n'est pas le meilleur choix pour les structures de données immuables. Mais il y a beaucoup d'autres collections, l'une d'elles est pcollections . Voyons ce qu'ils ont dans notre scénario:

@Benchmark fun pcollections(): List<Player> {
    val updatedPlayer = players[2].copy(score = 100)
    return pvector.with(2, updatedPlayer)
}

Où pvector est pvector:PVector<Player> = TreePVector.from(players).

$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark                             (size)   Mode  Cnt         Score         Error  Units
ModifyingImmutableList.baseline           10  thrpt  100  79462416.691 ± 1391446.159  ops/s
ModifyingImmutableList.baseline          100  thrpt  100  79991447.499 ± 1328008.619  ops/s
ModifyingImmutableList.baseline        10000  thrpt  100  80017095.482 ± 1385143.058  ops/s
ModifyingImmutableList.baseline      1000000  thrpt  100  81358696.411 ± 1308714.098  ops/s
ModifyingImmutableList.pcollections       10  thrpt  100  15665979.142 ±  371910.991  ops/s
ModifyingImmutableList.pcollections      100  thrpt  100   9419433.113 ±  161562.675  ops/s
ModifyingImmutableList.pcollections    10000  thrpt  100   4747628.815 ±   81192.752  ops/s
ModifyingImmutableList.pcollections  1000000  thrpt  100   3011819.457 ±   45548.403  ops/s

De bons résultats! Sur 1 million de cas, l'exécution n'est que 27 fois plus lente, ce qui est plutôt cool, mais pour les petites collections pcollections un peu plus lente que ArrayList mise en oeuvre.

Update: comme @ mfulton26, dans toMutable benchmark toList n'est pas nécessaire, je le supprime et je relance les tests. J'ajoute également un repère sur le coût de création TreePVector à partir d'un tableau existant

$ Java -jar target/benchmarks.jar  ModifyingImmutableList
Benchmark                                 (size)   Mode  Cnt         Score         Error  Units
ModifyingImmutableList.baseline               10  thrpt  200  77639718.988 ± 1384171.128  ops/s
ModifyingImmutableList.baseline              100  thrpt  200  75978576.147 ± 1528533.332  ops/s
ModifyingImmutableList.baseline            10000  thrpt  200  79041238.378 ± 1137107.301  ops/s
ModifyingImmutableList.baseline          1000000  thrpt  200  84739641.265 ±  557334.317  ops/s

ModifyingImmutableList.iterative              10  thrpt  200   7389762.016 ±   72981.918  ops/s
ModifyingImmutableList.iterative             100  thrpt  200    956362.269 ±   11642.808  ops/s
ModifyingImmutableList.iterative           10000  thrpt  200     10953.451 ±     121.175  ops/s
ModifyingImmutableList.iterative         1000000  thrpt  200       115.379 ±       1.301  ops/s

ModifyingImmutableList.pcollections           10  thrpt  200  15984856.119 ±  162075.427  ops/s
ModifyingImmutableList.pcollections          100  thrpt  200   9322011.769 ±  176301.745  ops/s
ModifyingImmutableList.pcollections        10000  thrpt  200   4854742.140 ±   69066.751  ops/s
ModifyingImmutableList.pcollections      1000000  thrpt  200   3064251.812 ±   35972.244  ops/s

ModifyingImmutableList.pcollectionsFrom       10  thrpt  200   1585762.689 ±   20972.881  ops/s
ModifyingImmutableList.pcollectionsFrom      100  thrpt  200     67107.504 ±     808.308  ops/s
ModifyingImmutableList.pcollectionsFrom    10000  thrpt  200       268.268 ±       2.901  ops/s
ModifyingImmutableList.pcollectionsFrom  1000000  thrpt  200         1.406 ±       0.015  ops/s

ModifyingImmutableList.toArrayList            10  thrpt  200  34567833.775 ±  423910.463  ops/s
ModifyingImmutableList.toArrayList           100  thrpt  200  11395084.257 ±   76689.517  ops/s
ModifyingImmutableList.toArrayList         10000  thrpt  200    134299.055 ±     602.848  ops/s
ModifyingImmutableList.toArrayList       1000000  thrpt  200       549.064 ±      15.317  ops/s

ModifyingImmutableList.toMutable              10  thrpt  200  32441627.735 ±  391890.514  ops/s
ModifyingImmutableList.toMutable             100  thrpt  200  11505955.564 ±   71394.457  ops/s
ModifyingImmutableList.toMutable           10000  thrpt  200    134819.741 ±     526.830  ops/s
ModifyingImmutableList.toMutable         1000000  thrpt  200       561.031 ±       8.117  ops/s
7
IRus

L'interface List de Kotlin permet "l'accès en lecture seule" à des listes qui ne sont pas nécessairement immuables. L’immuabilité ne peut pas être imposée via des interfaces. L'implémentation actuelle de stdlib de Kotlin pour toList appelle, dans certains cas, toMutableList et renvoie son résultat sous la forme d'un "accès en lecture seule" List.

Si vous avez une List de joueurs et souhaitez obtenir efficacement une autre List de joueurs avec un élément mis à jour, une solution simple consiste à copier la liste dans une MutableList , à mettre à jour l'élément souhaité, puis à ne stocker liste résultante utilisant "l'interface en lecture seule" List de Kotlin:

val updatedPlayers: List<Player> = players.toMutableList().apply {
    this[2] = updatedPlayer
}

Si vous avez souvent l'intention de le faire, vous pouvez envisager de créer une fonction d'extension pour encapsuler les détails de la mise en œuvre:

inline fun <T> List<T>.copy(mutatorBlock: MutableList<T>.() -> Unit): List<T> {
    return toMutableList().apply(mutatorBlock)
}

Vous pouvez ensuite copier les listes avec les mises à jour plus facilement (comme pour la copie de classes de données) sans avoir à spécifier explicitement le type de résultat:

val updatedPlayers = players.copy { this[2] = updatedPlayer }
5
mfulton26

Edit: Avec votre question mise à jour, je dirais que l'utilisation de map- like est la manière la plus performante de le faire, car elle ne copie la liste qu'une seule fois.


Si vous utilisez mutableListOf ou des constructeurs normaux tels que ArrayList() pour créer l'instance, vous pouvez simplement convertir List en MutableList:

val mp = players as MutableList<Player>
mp[2] = mp[2].copy(score = 100)

toList/toMutableList va dupliquer les éléments de la liste, vous avez donc raison pour l'impact sur les performances.

Cependant, l'idée est que si vous nécessité mutabilité, vous déclarez la propriété en tant que MutableList. Vous pouvez utiliser une construction comme celle-ci - en utilisant deux propriétés - si vous devez exposer la liste à un autre objet:

private val _players = mutableListOf<Player>()
val players: List<Player> 
       get() = _players.toList()

Pour la variable score, elle est similaire. Si vous devez la modifier, vous pouvez la déclarer en tant que var:

data class Player(val name: String, var score: Int = 0)

Dans ce cas, vous pouvez aussi simplement conserver la liste immuable et simplement mettre à jour la valeur:

players[2].score = 100

Vous pouvez trouver plus de détails sur les collections dans les docs: https://kotlinlang.org/docs/reference/collections.html

1
Lovis