ScatterMap,更好用的 HashMap(一)

本文翻译自 Google 工程师 Romain Guy 的个人博客,Romain 是 Android 创始人级别的工程师,大家应该可以在每年的 Google IO 上都见到他的身影。

原文地址: www.romainguy.dev/posts/2024/...

Hashmap 是一种非常常见的数据结构,不出意外 Jetpack Compose 在编写的时候也有大量用到。Kotlin 可以通过调用mutableMapOf()轻松创建一个新的可变 Hashmap,然后根据需要使用映射(map),大多数开发者基本上也都这样用。然而,在搞 Jetpack Compose 这样的工具包时,我们想尽量降低工具包对应用程序性能的影响。

所以让我们先看一下mutableMapOf()的实现:

kotlin 复制代码
public inline fun <K, V> mutableMapOf(): MutableMap<K, V> = LinkedHashMap()

LinkedHashMap 是一个 expected类,在 Android 平台的实现取决于 Java 的 同名类:

kotlin 复制代码
public actual typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>

由于这个 API 身经百战,见得多了,在 Kotlin 中作为标准 Hashmap 确实是个不错的选择^1^。然而LinkedHashMap 的实现并不算"内存友好",因为每个插入到映射(map)中的条目都会创建一个新的 LinkedHashMapEntry 实例(对于 HashMapHashMap.Node 这样的基类也一样),在我们的使用场景之下,就带来了一些负面影响:

  • 每个条目使用的内存比必要的多。在 LinkedHashMap 中存储一个 Int 需要额外的 3 个指针以及哈希副本,外加一个对象的开销。
  • 内存访问,特别是调用迭代(iterations)时不够友好,尤其是随着堆内存的混乱而逐渐加剧。
  • 增大 GC 的压力,尤其在旧设备和/或旧版本的 Android Runtime(ART)上跑的时候压力山大。

为了解决这些问题,我们开发了一个名为 ScatterMap 的新Hashmap,旨在更加高效利用内存。这个新的 API 已经作为 androidx.collection 库的一部分,并在版本 1.4 中最近达到了候选发布(RC)状态。

ScatterMap 的一些特性包括:

  • 每个条目的开销为 1 字节。
  • 除非映射需要扩展其内部存储,否则插入不会产生分配。
  • 迭代(iterations)无需额外分配内存。
  • 常见 API 的实现都无需额外分配内存(例如,removeIf()inline 的,以避免分配 predicate lambda)。
  • 键(Key)和值(Value)被线性存储,当在映射(map)上进行迭代时,可以更好地进行内存缓存,无需额外的间接引用。
  • 在插入、移除、检索和迭代方面的性能要优于LinkedHashMap,或至少与 LinkedHashMap 相当。
  • API 与 Kotlin 的映射(map)一致(工厂函数、ScatterMapMutableScatterMap 等)。
  • 提供专门的变体以存储基本类型(例如 IntIntMapObjectFloatMap),无需自动装箱。

以下是在一台运行 Android 13 的 Pixel 6 手机上的性能跑分数据^2^:

测试项 LinkedHashMap ScatterMap
插入 1000 个元素 43,073 ns 24,654 ns
移除 1000 个元素 6,642 ns 7,840 ns
迭代 1000 个元素 10,308 ns 4,044 ns
查询 1000 个元素^3^ 9,832 ns 10,678 ns
内存分配^4^ 1,003 4

请注意,在这个测试前提之下,LinkedHashMap 的迭代测试成绩已经是它能够跑出的最高分数了,因为所有元素都是一个接一个立刻创建的,这就使得它们具有了良好的内存局部性。而 ScatterMap 的性能则能够做到随时间保持不变。

ScatterMap 的主要缺点是它没有实现标准的 Map 接口,这会导致不友好的内存行为。不过你可以通过调用 asMap() 方法从 ScatterMap 中获取一个 Map或者 MutableMap,因为默认的 API 尽量提供了很多 Map 中的常见功能,如 forEach()removeIf()any()count() 等。

所以,ScatterMap 是更好的 HashMap 吗?答案是肯定的,如果你需要优化内存分配、内存使用和内存访问,我认为它是值得一试的,你的职责就是根据实际使用场景去选择最终要用哪种数据结构。

在未来的文章中,我将解释 ScatterMap 的由来,它的内部工作原理,以及如何优化为生成高效的 ARM 64 汇编^5^,以及如何利用 SIMD 指令进一步提升性能。

  1. 我其实对这个结论也持保留意见,LinkedHashMap 保证了可预测的迭代顺序,如果你依赖这种行为,在实际开发过程中接受 MapMutableMap 这样的泛型类型,可能会导致一些微妙的错误。
  2. 这些是最初的跑分数据,在这之后 ScatterMap 有进行一些小的优化。︎
  3. 方法就是调 1000 次get(key: K)
  4. 指的是插入 1000 个元素。
  5. 为此我还写了一个 kotlin-explorer的反汇编工具。 .
相关推荐
欧阳枫落2 分钟前
python 2小时学会八股文-数据结构
开发语言·数据结构·python
手握风云-15 分钟前
零基础Java第十六期:抽象类接口(二)
数据结构·算法
<但凡.1 小时前
编程之路,从0开始:知识补充篇
c语言·数据结构·算法
f狐0狸x2 小时前
【数据结构副本篇】顺序表 链表OJ
c语言·数据结构·算法·链表
guoruijun_2012_42 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood2 小时前
一文了解Android中的AudioFlinger
android·音频
Tmbcan2 小时前
zkw 线段树-原理及其扩展
数据结构·zkw 线段树
2301_801760932 小时前
数据结构--PriorityQueue
数据结构
乐悠小码2 小时前
数据结构------队列(Java语言描述)
java·开发语言·数据结构·链表·队列
B.-4 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio