【问题标题】:Kotlin: Update Immutable List ElementKotlin:更新不可变列表元素
【发布时间】:2016-10-21 20:30:44
【问题描述】:

这里是 Kotlin 初学者。如何获取一个列表而不改变它,创建第二个(不可变的)列表,其中包含特定索引处的一个更新元素?

我正在考虑两种方法,这两种方法似乎都可能导致性能下降、改变底层对象,或两者兼而有之。

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 标准库或其他库中是否有更合适的数据结构? Kotlin 似乎没有向量。

【问题讨论】:

  • 你找到答案了吗?

标签: kotlin purely-functional


【解决方案1】:

对我来说,显然第二种方式应该更快,但要快多少?

所以我写了一些基准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 方法的吞吐量为每秒 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 不是不可变数据结构的最佳选择。但是还有很多其他的收藏,其中之一是pcollections。让我们看看他们在我们的场景中得到了什么:

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

其中 pvector 是pvector:PVector&lt;Player&gt; = 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 实现慢一点。

更新:正如@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

【讨论】:

  • 不错的基准测试,但似乎在setup 中完成了一些基准测试,这些基准测试应该在基准测试本身中完成以比较苹果和苹果。 github.com/KotlinBy/kotlin-benchmarks/commit/…
  • 不,因为我们不衡量创建集合的成本,而是衡量修改的成本。在这种情况下,有正确的收集是要求。根据@Eric 所做的事情,他可以创建PVector 而不是List,因此不需要包装。对于可变情况也是如此。 TreePVector.from 的成本可以作为另一个基准。
  • 我明白了。谢谢@IRus。根据我从 Eric 的问题中了解到的情况,他有一个列表,并希望创建一个包含更新元素的副本,这就是为什么我认为创建副本的成本应该包含在每个基准测试中。
  • 请注意,toMutable 基准测试中对toList 的调用是不必要的。 MutableList 已经是 List。无需复制列表即可返回 List
  • 所以看起来你的答案是,“对于大型集合,使用PVector 开头(不要从列表转换)是获得不错的不可变性能的唯一方法。对于小型集合,请使用PVectortoMutabletoArrayList。”这是你的建议吗?
【解决方案2】:

Kotlin 的List 接口用于对不一定是不可变列表的列表进行“只读访问”。不能通过接口强制执行不变性。 Kotlin 的 stdlib 的 current implementation 用于 toList 调用,在某些情况下,toMutableList 并将其结果作为“只读访问”List 返回。

如果您有一个List 的玩家并希望有效地获得另一个具有更新元素的玩家List,那么一个简单的解决方案是将列表复制到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 }

【讨论】:

    【解决方案3】:

    我不明白为什么要比较这两种方法的相应性能。在第一个中,您遍历集合的所有元素,在第二个中,您通过索引直接找到所需的元素。 遍历不是免费的。

    【讨论】:

      【解决方案4】:

      编辑: 对于您更新的问题,我想说使用map-like 操作是执行此操作的最高效方式,因为它只复制列表一次。


      如果您使用mutableListOfArrayList() 等普通构造函数来创建实例,您可以简单地将List 转换为MutableList

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

      toList/toMutableList 将复制列表项,因此您对性能影响是正确的。

      然而,这个想法实际上是,如果您需要可变性,则将属性声明为 MutableList。 如果您需要将列表公开给另一个对象,您可以使用这样的构造 - 使用两个属性:

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

      score 变量也是类似的——如果需要更改,可以将其声明为var

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

      在这种情况下,您也可以只保留不可变的 List 并只更新值:

      players[2].score = 100
      

      您可以在文档中找到有关集合的更多详细信息:https://kotlinlang.org/docs/reference/collections.html

      【讨论】:

      • “在 Kotlin 中,列表总是可变的”这根本不是真的。如果您调用变异方法,则可能会出现异常,因为基础列表可能是任何东西,例如一个EmptyList
      • @KirillRakhman 你说得对,我实际上只是指用mutableListOf 或标准构造函数创建的列表 - 更新了答案
      • 很高兴知道:listOf(当前)调用 java 的 Arrays.asList,它实际上创建了这个不是真正 ArrayList 的奇怪 ArrayList,因为出于性能原因它使用数组作为支持数组。
      • 抱歉,“更新不可变列表中的元素”是指创建第二个不可变列表,其中更新了一个元素而不修改原始列表。我已经澄清了这个问题。
      猜你喜欢
      • 1970-01-01
      • 2019-02-26
      • 1970-01-01
      • 1970-01-01
      • 2016-01-27
      • 2019-06-13
      • 2020-05-14
      • 2012-09-06
      • 2012-10-15
      相关资源
      最近更新 更多