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 实例(对于 HashMap 、HashMap.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)一致(工厂函数、ScatterMap 与 MutableScatterMap 等)。
  • 提供专门的变体以存储基本类型(例如 IntIntMap 或 ObjectFloatMap),无需自动装箱。

以下是在一台运行 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 保证了可预测的迭代顺序,如果你依赖这种行为,在实际开发过程中接受 Map 或 MutableMap 这样的泛型类型,可能会导致一些微妙的错误。
  2. 这些是最初的跑分数据,在这之后 ScatterMap 有进行一些小的优化。︎
  3. 方法就是调 1000 次get(key: K) 。
  4. 指的是插入 1000 个元素。
  5. 为此我还写了一个 kotlin-explorer的反汇编工具。 .
相关推荐
longxibo6 小时前
【AIBIZ低代码平台:能长出业务系统的平台】
android·低代码·rxjava
AI_Gump14 小时前
Harness篇|DeepSeek Harness 上手机:五条安卓移植路线与 App 内 Linux 拆解
android·linux·人工智能·智能手机·deepseek
疯华整猫20 小时前
否决 Flutter 提案那天,我发现自己也说不清 Framework 管什么
android
nice_lcj52021 小时前
LRU 缓存入门:它是什么、怎么用、面试怎么手写
java·数据结构·缓存·面试·lru
꧁细听勿语情꧂21 小时前
直接选择排序,堆排序,冒泡排序,递归快速排序、hoare版本、挖坑法、lomuto前后指针,非递归快速排序
c语言·开发语言·数据结构·算法
传奇开心果编程1 天前
【Flutter踩坑排雷】第25课:ScrollController 误用坑|跳转失效、监听泄漏与“控制器未附加”
android·flutter·ios·移动开发·跨平台·dart·声明式ui
Pniubi1 天前
关于算法用现成数据结构和手撕
数据结构
慕容雪_1 天前
Capacitor8-Android打包完整手册
android·打包·capacitor
阿杰m2 天前
Android源码第十三章:Bluetooth——沿一次发现走到控制器
android
booboosui2 天前
派活给另一个 AI:前后两次文件夹快照,就是交付清单
android