web-dev-qa-db-ja.com

Kotlin:不変リスト要素の更新

コトリン初心者はこちら。リストを取得して変更せずに、特定のインデックスで1つの更新された要素を含む2番目の(不変)リストを作成するにはどうすればよいですか?

私は2つの方法を考えています。どちらの方法でも、パフォーマンスヒットが発生するか、基になるオブジェクトが変更されるか、またはその両方が発生する可能性があります。

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()

これを実行する方法がない場合、Kotlin stdlibまたは他のライブラリに、より適切なデータ構造がありますか? Kotlinにはベクターがないようです。

16
Eric

私にとっては、2番目の方法の方が高速であることは明らかですが、どれくらいですか?

だから私はいくつかのベンチマークを書いた here

_@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)
    }
}
_

そして、次のようになりました results

_$ 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
_

したがって、このテストは、コレクションの反復処理がコピーの3〜6倍遅くなることを示しています。また、実装を提供します。 toArray は、よりパフォーマンスが高いように見えます。

10要素では、toArrayメソッドのスループットは、1秒あたり_33278431.127 ± 560577.516_操作です。遅いですか?それとも非常に高速ですか? 「ベースライン」テストを作成します。これは、Playersのコピーと配列の変更のコストを示しています。興味深い結果:

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

変更可能な場所-MutableList、つまり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
_

10個の要素で2倍の回帰、100万個で約150000倍の回帰があります。

したがって、ArrayListのように見えますが、不変のデータ構造には最適ではありません。しかし、他にもたくさんのコレクションがあり、そのうちの1つは pcollections です。私たちのシナリオで彼らが得たものを見てみましょう:

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

ここで、pvectorは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
_

いい結果です! 100万件の場合、実行速度は27倍しかなく、これはかなりクールですが、小さなコレクションではpcollections実装よりも少し遅くArrayList遅くなります。

Update:@ mfulton26で述べたように、toMutableベンチマークではtoListは不要なので、削除してテストを再実行しました。また、既存のアレイからの作成コストTreePVectorのベンチマークも追加しました。

_$ 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
_
11
Ruslan

Kotlinの List インターフェースは、必ずしも不変のリストではないリストへの「読み取り専用アクセス」用です。インターフェースを介して不変性を強制することはできません。 Kotlinのstdlibの 現在の実装toList を呼び出し、場合によっては toMutableList を呼び出し、その結果を「読み取り専用アクセス」Listとして返します。

プレーヤーのListがあり、更新された要素を使用して別のListのプレーヤーを効率的に取得したい場合、1つの簡単な解決策は、リストを MutableList にコピーし、目的の要素を更新して、 Kotlinの「読み取り専用アクセス」Listインターフェースを使用した結果リストへの参照:

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

これが頻繁に行う予定の場合は、実装の詳細をカプセル化する拡張関数を作成することを検討してください。

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

次に、結果タイプを明示的に指定する必要なく、更新を含むリストをより滑らかにコピーできます(データクラスのコピーと同様)。

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

これらの2つのアプローチが対応するパフォーマンスの点で比較される理由がわかりません。最初の1つでは、コレクションのすべての要素をトラバースします。2番目の1つでは、インデックスによって必要な要素に直接移動します。トラバーサルは無料ではありません。

0
yuranos87

編集:更新された質問では、mapのような操作を使用することがこれを実行するための最もパフォーマンスの高い方法だと思います。リストは1回だけコピーされます。


mutableListOfまたはArrayList()のような通常のコンストラクターを使用してインスタンスを作成している場合は、ListMutableListにキャストするだけです。

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

toList/toMutableListはリストアイテムを複製するため、パフォーマンスへの影響は適切です。

ただし、実際には、変更可能needの場合、プロパティをMutableListとして宣言するという考え方です。リストを別のオブジェクトに公開する必要がある場合は、次のような構成を使用できます(2つのプロパティを使用)。

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

score変数の場合も同様です。変更する必要がある場合は、varとして宣言しても問題ありません。

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

この場合、不変のリストを保持し、値を更新することもできます。

players[2].score = 100

コレクションの詳細については、次のドキュメントをご覧ください。 https://kotlinlang.org/docs/reference/collections.html

0
Lovis