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的反汇编工具。 .
相关推荐
Android-Flutter4 小时前
Compose 管理状态 详解
android·compose
春弦_5 小时前
自用算法整理二
数据结构·算法·图论
缓慢更新5 小时前
审批的加签与会签:多人意见怎么合
android·java·数据库·低代码·rxjava
我怀里的猫6 小时前
android-activity-launch-sequence
android
YB13758 小时前
Jetpack Compose 动画rememberInfiniteTransition
android
xxxiugou12311 小时前
链表算法题精讲:从两数相加到K组翻转
数据结构·c++·算法·链表
baidu_2474386111 小时前
Android 自定义竖直的虚线
android
传奇开心果编程12 小时前
【Jetpack Compose娓娓道来】 第25课:ViewModel就是UI界面的大脑
android·kotlin·移动开发·jetpack compose·编程语言理论·编程框架·声明式ui
马立杰12 小时前
PHP 未授权 RCE 应急响应:CVE-2026-109618 CVE-2026-109619 排查与加固实战
android·开发语言·php
NamoAmitabha12812 小时前
安装“编译器环境”(Android Studio)--deepseek--Cocos --Creator3.8.8
android·ide·android studio