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的反汇编工具。 .
相关推荐
月盈缺3 小时前
学习嵌入式的第二十二天——数据结构——双向链表
数据结构·学习·链表
科大饭桶4 小时前
C++入门自学Day14-- Stack和Queue的自实现(适配器)
c语言·开发语言·数据结构·c++·容器
技术liul5 小时前
使用安卓平板,通过USB数据线(而不是Wi-Fi)来控制电脑(版本1)
android·stm32·电脑
躲在云朵里`5 小时前
深入理解数据结构:从数组、链表到B树家族
数据结构·b树
_祝你今天愉快7 小时前
Android FrameWork - 开机启动 & Init 进程 初探
android
2501_916007477 小时前
iOS App 上架实战 从内测到应用商店发布的全周期流程解析
android·ios·小程序·https·uni-app·iphone·webview
TimeFine8 小时前
Android 邮件发送日志
android
杨过过儿8 小时前
【Task02】:四步构建简单rag(第一章3节)
android·java·数据库
Wgllss8 小时前
Kotlin 享元设计模式详解 和对象池及在内存优化中的几种案例和应用场景
android·架构·android jetpack
1白天的黑夜18 小时前
链表-24.两两交换链表中的结点-力扣(LeetCode)
数据结构·leetcode·链表