AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / coding / 问题 / 79594341
Accepted
Marat Tim
Marat Tim
Asked: 2025-04-27 03:50:32 +0800 CST2025-04-27 03:50:32 +0800 CST 2025-04-27 03:50:32 +0800 CST

ArrayList 与 LinkedList 在缓存局部性方面

  • 772

与 Java 中的 LinkedList 相比,缓存局部性如何影响 ArrayList 的性能?

我经常听说 ArrayList 在缓存局部性方面有优势,但我不太明白为什么。既然 Java 将对象作为引用存储在内存中,那么访问这两个列表中的元素是否都需要跳转到内存中的随机位置?

java
  • 3 3 个回答
  • 121 Views

3 个回答

  • Voted
  1. Best Answer
    rzwitserloot
    2025-04-27T09:01:42+08:002025-04-27T09:01:42+08:00

    稍等一下,让我们首先说明一下 Java 的工作原理,因为如果没有这些知识,您就无法理解关于“一切皆链接”这些相互矛盾的说法。

    在 Java 中,每个变量都是固定大小的,而且非常小。这非常方便,因为它允许我们做出一些宏大而笼统的声明,例如“每当调用一个方法时,每个参数都会被复制到你正在调用的方法中;该方法可以对这些副本进行任何操作,并且一旦该方法执行完毕,这些副本就会消失”。毕竟,如果你做出这样的声明,并且一个变量有 2GB 大,那么传递它会非常迅速地导致内存不足的错误。

    但是,这如何实现呢?当然List<String> list = enumerateEveryWordInTheCollectedWorksOfShakespeare();不是“固定的、小尺寸的”。

    这就是第二部分要讨论的:在 Java 中,有原语,也就是以下硬编码的类型列表:int,,,,,,,,。 (这个列表一直都是固定的,永远不会改变,你无法创建自己的原语 ^ 1 )——每一个原语都是“固定的、小尺寸的”(具体来说long,它们都是 64 位或更short小的)。其他所有东西都是引用。一个指针。bytedoublefloatcharboolean

    当你写:

    String x = "hello";
    

    那么说“x 是 hello”是错误的。它不对。x 是对它的一个引用。

    就像“Hello”是一栋房子(想想看:字符串可以任意长,可以是莎士比亚的全集,远非“固定长度”,而是小尺寸的),而 x 仅仅是地址簿中的一页。它是一条指示,告诉你如何到达那栋房子。有了这一页,如果你想知道房子是不是红色的,你就能做到——只要……去那里看看就行。你所需要的只是这一页。但你确实需要花费一些时间,可以这么说。

    ArrayList

    ArrayList 正是“链接列表”的确切含义:根据定义,你不能拥有一个原始值的数组列表(你可以拥有一个List<Integer>;Integer它是带框的,即“引用”版本的int。List<int>在 Java 中是无效的),因此,它必须是一个引用列表。这意味着ArrayList 是一个地址簿。它是一个地址列表。

    该列表是“连续的”——也就是说,地址簿是完整的,并且按照您期望的顺序排列。如果您现在在地址簿的第 5 页,并且对第 6 页的地址感兴趣,那么我保证第 6 页就在附近。只需……翻过这一页,就在那里。保证。

    ArrayList 的一个缺点是,它和实际的地址簿一样,大小是固定的。那么,当你的地址簿写满时会发生什么呢?ArrayList 的实现会施展一些神秘的魔法,让列表看起来好像没有“哎呀,它满了”的问题:它会购买一本新的、更大的地址簿,手动复制旧地址簿中的每个地址,迅速用这本新地址簿替换你的地址簿,然后将旧地址簿扔进垃圾桶,所有这一切都是其正常操作的一部分。调用方法与其说是对对象“做”某事,不如说是要求对象为你做这件事。ArrayList 是一个地址簿,它能够理解如何出去,购买一本新的更大的地址簿,将自己复制到新的地址簿中,将自己的意识转移到新的地址簿中,然后将旧的自己扔进垃圾桶。

    LinkedList

    LinkedList 就像一本地址簿,你可以把它撕下来,然后把它们散落在房间里的各个角落。你知道地址簿的第一页在哪里,但也就仅此而已了。幸运的是,每一页都列出了你隐藏下一页和上一页的位置。所以,假设你想去地址簿第五页的房子,因为你想知道它是什么颜色的,你找到第一页,上面写着“你把第二页塞到沙发后面了”,你找到第二页,上面写着“第三页在冰箱顶上”,你找到第三页,上面写着“第四页在桌子上”,第四页写着“第五页也在桌子上”,最后你就可以去那栋房子了。

    这个过程真是太耗时了。但它有一个好处:你永远不需要买一本新的通讯录,再把旧的满满当当的抄过来。毕竟,有了这个散页系统,如果页数不够了,只要在房间里再放几张空白页就可以了。这虽然不是什么大优点,但我想也算是一个优点吧。

    现在,如果你碰巧创建了一个链表,并立即填满了其中的前 500 页,那么很可能所有 500 页仍然堆放在你的桌子上,或多或少地按顺序排列。但因为你无法确定,所以即使情况如此,如果你想去第 250 页列出的房子,你仍然必须依次浏览每一页,而使用 arraylist 地址簿,你可以一次性直接跳到第 250 页。

    但 LinkedList 并不要求你创建的时候就把它填满,如果时间一长,就会出现“页面乱七八糟”的情况。

    ...情况变得更糟!

    我们链表地址簿的每一页实际上存储了三项内容,而不是一项。它存储了上一页的位置、下一页的位置,当然还有房屋的地址。每一项都是一个“固定的小东西”(引用)。但这三项加在一起——这可不是 Java 的工作原理,太多了。

    因此,实际上 LinkedList 更像是“地址簿的每一页实际上是一本小小的地址簿,里面有 3 页,第一页解释前一个帖子在哪里,第一页解释下一个帖子在哪里,第一页是房子的地址”,这些迷你地址簿到处都是,还有一堆解释在哪里可以找到迷你地址簿的贴子。

    现在回到 Java 的工作原理

    LinkedList 由对第一个节点和最后一个节点的引用组成。节点是一个小对象,它包含对下一个节点、上一个节点以及列表中此条目所代表的对象的引用。例如,要遍历链表的前 10 个元素并全部打印出来,JVM 必须解析对链表的引用,从那里解析对第一个节点的引用,从那里解析对对象的引用并打印出来,然后解析对下一个节点的引用,解析对对象的引用并打印出来,解析对下一个节点的引用,依此类推。

    每个小节点对象都是在你添加对象时创建的,除非你恰好一次性完成所有操作,否则这些节点对象会散落在堆的各个角落。当然,列表中包含的实际对象也可能散落在堆的各个角落,这取决于它们的创建时间。

    相比之下,使用ArrayList,你只需保证对列表中包含的实际对象的引用是连续的。这些对象可能不是连续的,但至少对它们的引用是连续的。

    那么这意味着什么?

    它可以归结为两个词,这就是作为一名 Java 程序员你必须了解的有关链表的全部内容。

    LinkedList 不好。

    就是这样。就是这样。LL 是正确答案的情况少之又少。ArrayList 通常更好,但并非总是如此;然而,在几乎所有可以想象到的用例中,其他一些集合变体都会比 LL 更好。

    各种与语言无关的 LL 观点都声称它们可能适用于 LinkedList,但不适用于Java的观点。例如,“链表的优点是,给定列表中的一个对象,可以在其后快速插入一个对象”。这是错误的——给定链表中的一个条目,您无法在 Java 中“返回”到该链表的节点。本质上,在 Java 中开始享受 LL 好处的唯一方法是使用很少使用的.listIterator()方法,这种方法确实可以以 ArrayList 无法比拟的方式快速插入值。没有 listIterator,LL 唯一能做而 AL 做不到的事情就是“从开始和结束快速添加/删除”。

    但如果这就是您想要的,ArrayDeque那么使用“可以让您从任一端快速添加/删除的列表”会更加高效。


    [1] Valhalla 和 Panama 的诸多方面意味着这些声明很快就会需要大量附加说明。从 JDK24 开始,这一点是准确的。如果你知道 Valhalla 和 Panama 项目是什么——是的,改变即将到来。

    • 3
  2. Alex
    2025-04-27T04:01:41+08:002025-04-27T04:01:41+08:00

    是的,ArrayList 将引用存储在连续的数组中,从而加快了 CPU 缓存的顺序访问速度。LinkedList 节点分散在内存中,导致更多缓存未命中。

    • 1
  3. Grebla Andrei
    2025-04-27T04:15:58+08:002025-04-27T04:15:58+08:00

    我将用两种方式来回答这个问题:一种是技术性的回答,另一种是简单易懂的例子。在回答这个问题之前,我必须做一些研究,简化问题总是对我有帮助,所以让我们先介绍一下缓存术语:

    缓存:缓存就像一张专门的阅览桌,上面放着最常用的书籍。计算机不用每次都跑到书架深处,而是可以从阅览桌里抓取特定的书籍。

    那么这与您的问题有何关系?

    ArrayList将数据存储在阅读桌上的一行中。因此,当计算机阅读一本书时,它附近已经有下一本书了,这使得阅读速度非常快且高效。因此,顺序访问速度更快。

    LinkedList就像一套散落的书,你必须按照注释找到下一本书。每当计算机需要一本新书时,它就必须穿过另一个书架。

    现在你可能想要一个更“技术性”的答案,所以这里是:

    在ArrayList中:

    由于所有元素都在同一个缓存块中,访问一个元素时会自动将附近的元素加载到缓存中。这显然会使迭代速度更快,因为 CPU 一次可以检索更多元素。

    在 LinkedList 中:

    由于节点分散在内存中,访问一个节点并不能保证下一个节点就在附近。每次访问节点都涉及指针解引用,从而导致更高的缓存未命中率。

    总而言之:当迭代 LinkedList 时,对象引用和下一个节点引用都需要解析,这通常会导致比 ArrayList 更多的缓存未命中,其中下一个元素只是内存中的下一个位置。

    这篇文章对我有帮助:为什么缓存局部性对数组性能如此重要? Stack Overflow 上的这个讨论与此类似。它深入探讨了数组相对于链表的缓存局部性优势。

    • 0

相关问题

  • Lock Condition.notify 抛出 java.lang.IllegalMonitorStateException

  • 多对一微服务响应未出现在邮递员中

  • 自定义 SpringBoot Bean 验证

  • Java 套接字是 FIFO 的吗?

  • 为什么不可能/不鼓励在服务器端定义请求超时?

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve