【问题标题】:What is the data structure behind NSMutableArray?NSMutableArray 背后的数据结构是什么?
【发布时间】:2014-03-23 22:56:52
【问题描述】:

通常,“可变数组”类被实现为简单数组的包装器。当您在末尾添加元素时,包装器会分配更多内存。这是一种常见的数据结构,各种操作的性能是众所周知的。您在数组末尾获得 O(1) 元素访问、O(N) 插入和删除,或 O(1)(平均)插入和删除。但是NSMutableArray 是另一回事。例如docs 说[强调我的]:

注意:数组上的大多数操作需要恒定时间:访问元素、添加或删除元素在任一端 ,并替换一个元素。将元素插入到数组的中间需要线性时间。

那么,NSMutableArray 到底是什么?这是在某处记录的吗?

【问题讨论】:

  • AFAIK,它没有正式记录。通常使用以下两种方法之一:调整大小和复制,或多个数组,每个数组都覆盖范围的一部分。而且我认为 Apple 并不想正式承诺特定的设计,以便赋予他们重新设计的能力。
  • 查看任何实现代码的最佳机会是从苹果搜索 CFLite 代码。至少,你可以在那里找到 CFArray。
  • 只是猜测:可变数组以某种方式实现,即数组使用的内存块的末尾和开头有一些空闲内存。这就是在开头插入只需要恒定时间的方式。但是他们也写到其实不只是一个类,所以我也猜对不同的大小有不同的优化

标签: objective-c data-structures nsmutablearray foundation


【解决方案1】:

它是circular buffer 的包装器。

这既没有记录也没有开源,但this blog post 展示了一个惊人的逆向工程工作,而不是NSMutableArray,我认为你会发现它非常有趣。

NSMutableArray 类集群由名为 __NSArrayM 的具体私有子类支持。

最大的发现是 NSMutableArray 不是 CFArray 的薄包装,正如人们可能合理地认为的那样:CFArray 是开源的,它不使用循环缓冲区,而 __NSArrayM 使用.

通读文章的 cmets,似乎从 iOS 4 开始就是这样,而在以前的 SDK 中,NSMutableArray 实际上在内部使用了CFArray,而__NSArrayM 甚至不存在。

直接来自我上面提到的博客文章

数据结构

您可能已经猜到了,__NSArrayM 使用循环缓冲区。 这个数据结构非常简单,但是有点多 比常规数组/缓冲区复杂。通函内容 当到达任一端时,缓冲区可以回绕。

环形缓冲区有一些很酷的特性。值得注意的是,除非 缓冲区已满,从任一端插入/删除不需要任何 要移动的内存。

objectAtIndex: 的伪代码如下:

- (id)objectAtIndex:(NSUInteger)index {
    if (_used <= index) {
        goto ThrowException;
    }

    NSUInteger fetchOffset = _offset + index;
    NSUInteger realOffset = fetchOffset - (_size > fetchOffset ? 0 : _size);

    return _list[realOffset];

ThrowException:
    // exception throwing code
}

其中 ivars 被定义为

  • _used: 数组包含的元素个数
  • _list: 指向循环缓冲区的指针
  • _size: 缓冲区大小
  • _offset: 数组第一个元素在缓冲区的索引

再一次,我不相信以上所有信息,因为它们直接来自amazing blog post by Bartosz Ciechanowski

【讨论】:

  • @MartinR 引用我引用的博客文章的作者的话:我很震惊地发现NSArrayNSMutableArrayCFArray 没有任何共同之处跨度>
  • @MartinR 最值得注意的是,CFArray 不使用循环缓冲区。
  • @gasher729 当然。如果您查看我链接的文章,它也进行了一些测试,结果是NSMutableArray 任一端的插入和删除都是在恒定时间内执行的,使其适合用作队列。跨度>
  • @GabrielePetronella "CFArray 是一个 C 数据结构,因此它不能是 NSArray 的子类" - 两者是免费桥接是有原因的。无论如何,Objective-C 对象和类“只是”C 结构。 至少这两种类型的布局的“公共”部分是相同的。另外,尝试运行this——我有NSMutableArray,所以@我的 Mac 上的 987654350@NSMutableArray 的具体子类。这并不意味着它们的实现不能不同,但仍然要注意你的措辞。
  • 我记得 bbum 在评论中提到 SO,令人惊讶的是,“桥接”方向在过去几年中发生了变化——一些?最多? CF 现在实际上已经在 ObjC 中实现了。但是,我不清楚这如何与开源 CF 匹配。
【解决方案2】:

做了一些测量:从一个空数组开始,添加 @"Hello" 100,000 次,然后删除它 100,000 次。不同的模式:在末尾添加/删除,在开头,在中间,靠近开始(尽可能在索引 20 处),接近结尾(尽可能远离结尾 20 个索引),以及我交替的一个在接近开始和结束之间。以下是 100,000 个对象的时间(在 Core 2 Duo 上测量):

Adding objects = 0.006593 seconds
Removing objects at the end = 0.004674 seconds
Adding objects at the start = 0.003577 seconds
Removing objects at the start = 0.002936 seconds
Adding objects in the middle = 3.057944 seconds
Removing objects in the middle = 3.059942 seconds
Adding objects close to the start = 0.010035 seconds
Removing objects close to the start = 0.007599 seconds
Adding objects close to the end = 0.008005 seconds
Removing objects close to the end = 0.008735 seconds
Adding objects close to the start / end = 0.008795 seconds
Removing objects close to the start / end = 0.008853 seconds

所以每次添加/删除的时间与到数组开头或结尾的距离成正比,以较近者为准。在中间添加东西是昂贵的。您不必在最后完全工作;删除靠近开始/结束的元素也很便宜。

作为循环列表的建议实现省略了一个重要细节:在最后一个和第一个数组元素的位置之间存在可变大小的间隙。随着数组元素的添加/删除,该间隙的大小会发生变化。当间隙消失并添加更多对象时,需要分配更多内存并移动对象指针;当间隙变得太大时,可以缩小数组并且需要移动对象指针。一个简单的更改(允许间隙位于任何位置,而不仅仅是在最后一个元素和第一个元素之间)将允许任何位置的更改快速(只要它是相同的位置),并且会使操作“变薄” " 阵列更快。

【讨论】:

  • 您的结果证实了文档中所述的内容,但这不是问题(据我了解)。
  • 这完全符合规范。两端的恒定时间插入/删除,显然要付出中间插入/删除性能差的代价。话虽如此,我看不出这是如何回答原始问题的。
  • 那么为什么会被问到这个问题呢?空闲的好奇心?还是要了解性能特征?优化的黄金法则:如果你没有衡量它,它就不算数。顺便提一句。没有充分的理由假设 MacOS X 或 iOS 中的实现与开源实现相同。它也不完全是一个循环缓冲区,因为循环缓冲区可以轻松地在缓冲区的任何位置(例如中间)处理插入/删除。
  • @gnasher729 问题是NSMutableArray 背后的数据结构是什么。循环缓冲区来自实际实现的逆向工程,而不是来自开源代码(Foundation 不可用),所以这里没有人做出这样的假设。
  • 作为循环列表的建议实现。没有。循环列表和循环缓冲区不是一回事。 first 是一个链表,其中最后一个元素指向第一个元素。而缓冲区是一块内存,而在循环缓冲区中,偏移量定义了数据的开始位置。
猜你喜欢
  • 1970-01-01
  • 2013-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-26
  • 2015-02-03
  • 2015-12-09
相关资源
最近更新 更多