コトリン初心者はこちら。リストを取得して変更せずに、特定のインデックスで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にはベクターがないようです。
私にとっては、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
_
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 }
これらの2つのアプローチが対応するパフォーマンスの点で比較される理由がわかりません。最初の1つでは、コレクションのすべての要素をトラバースします。2番目の1つでは、インデックスによって必要な要素に直接移動します。トラバーサルは無料ではありません。
編集:更新された質問では、map
のような操作を使用することがこれを実行するための最もパフォーマンスの高い方法だと思います。リストは1回だけコピーされます。
mutableListOf
またはArrayList()
のような通常のコンストラクターを使用してインスタンスを作成している場合は、List
をMutableList
にキャストするだけです。
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