位图向量 trie 为何比普通向量更快?

发布于 2024-12-27 03:45:06 字数 242 浏览 3 评论 0原文

据说比向量更快,但我真的不这么认为了解引用的局部性应该如何帮助实现这一点(因为根据定义,向量是可能的最局部打包的数据——每个元素都紧挨着后继元素打包,之间没有额外的空间)。

基准测试是否假设特定的使用模式或类似的东西?

这怎么可能?

It's supposedly faster than a vector, but I don't really understand how locality of reference is supposed to help this (since a vector is by definition the most locally packed data possible -- every element is packed next to the succeeding element, with no extra space between).

Is the benchmark assuming a specific usage pattern or something similar?

How this is possible?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

小…红帽 2025-01-03 03:45:07

位图向量尝试严格来说并不比普通向量快,至少不是在所有方面都快。这取决于您正在考虑进行什么操作。

例如,传统向量在访问特定索引处的数据元素时速度更快。很难击败直接索引数组查找。从缓存局部性的角度来看,如果您所做的只是按顺序循环遍历大数组,那么大数组就非常好。

然而,位图向量 trie 对于其他操作来说会更快(由于结构共享) - 例如,在不影响原始数据结构的情况下,使用单个更改元素创建新副本的时间复杂度为 O(log32 n),而对于传统向量。这是一个巨大的胜利。

这是一个关于该主题的精彩视频,非常值得观看,其中包含了许多为什么您可能希望在您的语言中使用此类结构的动机:持久数据结构和托管引用(Rich Hickey 的演讲)。

bitmapped vector tries aren't strictly faster than normal vectors, at least not at everything. It depends on what operation you are considering.

Conventional vectors are faster, for example, at accessing a data element at a specific index. It's hard to beat a straight indexed array lookup. And from a cache locality perspective, big arrays are pretty good if all you are doing is looping over them sequentially.

However a bitmapped vector trie will be much faster for other operations (thanks to structural sharing) - for example creating a new copy with a single changed element without affecting the original data structure is O(log32 n) vs. O(n) for a traditional vector. That's a huge win.

Here's an excellent video well worth watching on the topic, which includes a lot of the motivation of why you might want these kind of structures in your language: Persistent Data Structures and Managed References (talk by Rich Hickey).

情深如许 2025-01-03 03:45:07

其他答案中有很多好东西,但没有人回答你的问题。 PersistenVector 仅对于按索引进行大量随机查找(当数组很大时)才快速。 “怎么可能?”你可能会问。 “普通的平面数组只需要移动一个指针,PersistentVector 必须经过多个步骤。”

答案是“缓存局部性”。

缓存总是从内存中获取一个范围。如果你有一个大数组,它不适合缓存。因此,如果您想获取项目 x 和项目 y,则必须重新加载整个缓存。这是因为数组在内存中始终是连续的。

现在有了 PVector,情况就不同了。有很多浮动的小数组,JVM 对此很聪明,并将它们放在内存中彼此靠近的位置。所以对于随机访问来说这很快;如果你按顺序运行它会慢得多。

我不得不说,我不是硬件或 JVM 如何处理缓存局部性方面的专家,而且我自己也从未对此进行过基准测试;我只是重述我从其他人那里听到的东西:)

编辑:mikera 也提到了这一点。

编辑 2:请参阅有关函数式数据结构的讨论,如果您只对向量感兴趣,请跳到最后一部分。 http://www.infoq.com/presentations/Functional-Data-Structures -在Scala中

There is a lot of good stuff in the other answers but nobdy answers your question. The PersistenVectors are only fast for lots of random lookups by index (when the array is big). "How can that be?" you might ask. "A normal flat array only needs to move a pointer, the PersistentVector has to go through multiple steps."

The answer is "Cache Locality".

The cache always gets a range from memory. If you have a big array it does not fit the cache. So if you want to get item x and item y you have to reload the whole cache. That's because the array is always sequential in memory.

Now with the PVector that's diffrent. There are lots of small arrays floating around and the JVM is smart about that and puts them close to each other in memory. So for random accesses this is fast; if you run through it sequentially it's much slower.

I have to say that I'm not an expert on hardware or how the JVM handles cache locality and I have never benchmarked this myself; I am just retelling stuff I've heard from other people :)

Edit: mikera mentions that too.

Edit 2: See this talk about Functional Data-Structures, skip to the last part if you are only intrested in the vector. http://www.infoq.com/presentations/Functional-Data-Structures-in-Scala

孤星 2025-01-03 03:45:07

位图向量 trie(又名持久向量)是 Rich Hickey 为 Clojure 发明的一种数据结构,自 2010 年以来已在 Scala 中实现(v 2.8)。正是其巧妙的按位索引策略允许高效地访问和修改大型数据集。

来自了解 Clojure 的持久向量

可变向量和ArrayList通常只是不断增长的数组
并在需要时收缩。当你想要可变性时,这非常有用,
但当你想要坚持时,这是一个大问题。你变慢了
修改操作,因为您必须复制整个数组
一直这样,并且会占用大量内存。这将是理想的
以某种方式尽可能避免冗余而不丢失
查找值时的性能以及快速操作。那
这正是 Clojure 的持久向量所做的,而且它已经完成了
通过平衡、有序的树。

这个想法是实现一个类似于二进制的结构
树。唯一的区别是树中的内部节点有
最多两个子节点的引用,并且不包含任何元素
他们自己。叶节点最多包含两个元素。元素
是有序的,也就是说第一个元素是第一个元素
在最左边的叶子中,最后一个元素是最右边的元素
最右边的叶子。现在,我们要求所有叶节点都位于
相同深度2。作为示例,请看下面的树:
其中的整数 0 到 8,其中 0 是第一个元素,8 是
最后的。数字 9 是向量大小:

“在此处输入图像描述"

如果我们想添加一个新元素到这个向量的末尾,我们
在可变世界中,我们将在最右边的叶子中插入 9
节点,像这样:

“在此处输入图像描述"

但问题是:如果我们想坚持下去,我们就无法做到这一点。
如果我们想更新一个元素,这显然行不通!
我们需要复制整个结构,或者至少是其中的一部分。

为了在保留完全持久性的同时最大限度地减少复制,我们执行路径
复制:我们将路径上的所有节点复制到我们想要的值
更新或插入,并用新值替换该值
在底部。多次插入的结果如下所示。这里,
具有 7 个元素的向量与具有 10 个元素的向量共享结构
元素:

“在此处输入图像描述"

粉色节点在向量之间共享,而
棕色和蓝色是分开的。其他未可视化的向量也可能
与这些向量共享节点。


更多信息

除了了解 Clojure 的持久向量 ,这种数据结构背后的想法及其用例在 David Nolen 2014 年的演讲中也得到了很好的解释不变性、交互性和安全性JavaScript,下面的屏幕截图就是从中获取的。或者,如果您确实想深入了解技术细节,另请参阅 Phil Bagwell 的 Ideal Hash Trees,这是 Hickey 最初的 Clojure 实现所基于的论文。

持久位图树

A bitmapped vector trie (aka a persistent vector) is a data structure invented by Rich Hickey for Clojure, that has been implementated in Scala since 2010 (v 2.8). It is its clever bitwise indexing strategy that allows for highly efficient access and modification of large data sets.

From Understanding Clojure's Persistent Vectors :

Mutable vectors and ArrayLists are generally just arrays which grows
and shrinks when needed. This works great when you want mutability,
but is a big problem when you want persistence. You get slow
modification operations because you'll have to copy the whole array
all the time, and it will use a lot of memory. It would be ideal to
somehow avoid redundancy as much as possible without losing
performance when looking up values, along with fast operations. That
is exactly what Clojure's persistent vector does, and it is done
through balanced, ordered trees.

The idea is to implement a structure which is similar to a binary
tree. The only difference is that the interior nodes in the tree have
a reference to at most two subnodes, and does not contain any elements
themselves. The leaf nodes contain at most two elements. The elements
are in order, which means that the first element is the first element
in the leftmost leaf, and the last element is the rightmost element in
the rightmost leaf. For now, we require that all leaf nodes are at the
same depth2. As an example, take a look at the tree below: It has
the integers 0 to 8 in it, where 0 is the first element and 8 the
last. The number 9 is the vector size:

enter image description here

If we wanted to add a new element to the end of this vector and we
were in the mutable world, we would insert 9 in the rightmost leaf
node, like this:

enter image description here

But here's the issue: We cannot do that if we want to be persistent.
And this would obviously not work if we wanted to update an element!
We would need to copy the whole structure, or at least parts of it.

To minimize copying while retaining full persistence, we perform path
copying: We copy all nodes on the path down to the value we're about
to update or insert, and replace the value with the new one when we're
at the bottom. A result of multiple insertions is shown below. Here,
the vector with 7 elements share structure with a vector with 10
elements:

enter image description here

The pink coloured nodes are shared between the vectors, whereas the
brown and blue are separate. Other vectors not visualized may also
share nodes with these vectors.


More info

Besides Understanding Clojure's Persistent Vectors, the ideas behind this data structure and its use cases are also explained pretty well in David Nolen's 2014 lecture Immutability, interactivity & JavaScript, from which the screenshot below was taken. Or if you really want to dive deeply into the technical details, see also Phil Bagwell's Ideal Hash Trees, which was the paper upon which Hickey's initial Clojure implementation was based.

Persistent bitmap trie

℡Ms空城旧梦 2025-01-03 03:45:07

“普通向量”是什么意思?只是一个平面的项目数组?如果您从不更新它,那很好,但如果您更改了 1M 元素的平面向量,则必须进行大量复制;树的存在是为了让您共享大部分结构。

What do you mean by "plain vector"? Just a flat array of items? That's great if you never update it, but if you ever change a 1M-element flat-vector you have to do a lot of copying; the tree exists to allow you to share most of the structure.

苦笑流年记忆 2025-01-03 03:45:07

简短的解释:它利用了 JVM 在读/写/复制数组数据结构上如此努力优化的事实。 IMO 的关键方面是,如果您的向量增长到一定大小,则索引管理将成为瓶颈。这里出现了来自持久向量的非常聪明的算法,在非常大的集合上它的性能优于标准变体。所以基本上它是一个函数式数据结构,它的性能之所以如此好,是因为它是建立在小型可变的高度优化的 JVM 数据结构上的。
更详细的内容请看这里(最后)
http://topsy.com/vimeo.com/28760673

Short explanation: it uses the fact that the JVM optimizes so hard on read/write/copy array data structures. The key aspect IMO is that if your vector grows to a certain size index management becomes a  bottleneck . Here comes the very clever algorithm from persisted vector into play, on very large collections it outperforms the standard variant. So basically it is a functional data-structure which only performed so well because it is built up on small mutable highly optimizes JVM datastructures.
For further details see here (at the end)
http://topsy.com/vimeo.com/28760673

捶死心动 2025-01-03 03:45:07

从演讲的标题来看,它谈论的是 Scala 向量,它们甚至与“可能的最本地打包数据”相距甚远:请参阅 https://lampsvn.epfl.ch/trac/scala/browser/scala/tags/R_2_9_1_final/src/library/scala/collection/immutable/Vector.scala

你的定义仅适用于Lisps(据我所知)。

Judging by the title of the talk, it's talking about Scala vectors, which aren't even close to "the most locally packed data possible": see source at https://lampsvn.epfl.ch/trac/scala/browser/scala/tags/R_2_9_1_final/src/library/scala/collection/immutable/Vector.scala.

Your definition only applies to Lisps (as far as I know).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文