研究!rsc公司

关于编程的想法和链接,通过

RSS公司

利用未初始化的记忆获取乐趣和利润
发布于2008年3月14日星期五。

这是一个关于一个聪明的把戏的故事至少35年,其中可以保留数组值未初始化,然后在正常操作期间读取,然而,无论什么垃圾,代码都能正确运行正在数组中。就像最好的编程技巧一样,这个工具是在某些情况下工作。未初始化数据的稀疏性访问被性能改进所抵消:一些重要的运算从线性变换到恒定时间。

阿尔弗雷德·阿霍(Alfred Aho)、约翰·霍普克罗夫特(John Hopcroft)和杰弗里·厄尔曼(Jeffrey Ullman)1974年的著作计算机算法的设计与分析练习中技巧的提示(第2章,练习2.12):

开发一种将矩阵条目初始化为零的技术第一次访问时,从而消除O(运行)(||V(V)||2)时间初始化邻接矩阵。

乔恩·本特利1986年的书编程珠玑展开关于练习(第1列,练习8;练习9第二版):

用更少的时间交换更多空间的一个问题是初始化空间本身会花费大量时间。展示如何通过设计技术来规避此问题第一次初始化向量的条目为零已访问。您的方案应使用恒定时间进行初始化以及每个矢量访问;你可以使用额外的空间比例向量的大小。因为这种方法减少了通过使用更多空间初始化时间,它应该只有在空间便宜、时间昂贵、向量是稀疏的。

Aho、Hopcroft和Ullman的练习谈到了矩阵和Bentley的练习涉及向量,但现在让我们考虑只是一组简单的整数。

一组n个整数范围从0到是位向量,在集合中与整数对应的位置。向集合添加新整数,删除整数并检查特定整数在集合中都是非常快速的恒定时间操作(每个只需几个位操作)。不幸的是,有两项重要操作进展缓慢:迭代集合中的所有元素需要时间O(运行)(),就像清除集合一样。如果常见的情况是这样远大于n个(也就是说,集合只是稀疏的填充)并迭代或清除集合经常发生,那么最好使用表示来进行这些操作效率更高。这就是诀窍所在。

普雷斯顿·布里格斯和琳达·托森1993年的论文,稀疏集的一种有效表示,”详细描述了技巧。他们的解决方案使用整数表示稀疏集已命名的数组稠密的和一个整数n个计算中的元素数稠密的.这个稠密的数组只是设置,按插入顺序存储。如果集合包含元素5、1和4,则n=3密实[0]=5,致密[1]=1,密实[2]=4:

一起n个稠密的足够的信息来重建集合,但是这个表示速度不是很快。为了快速,布里格斯和托尔松添加第二个名为的数组稀疏的它将整数映射到其索引稠密的.继续这个例子,稀疏[5]=0,稀疏[1]=1,稀疏[4]=2.本质上,集合是一对指向彼此:

向集合中添加成员需要更新这两个数组:

添加成员(i):密度[n]=i稀疏[i]=nn个++

它不如在位向量中翻转一点有效,但它是时间仍然很快而且恒定。

检查是否在集合中,您验证对于该元素,两个数组相互指向:

是成员(i):return sparse[i]<n&&密集[sparse[1i]]==i

如果那么不在集合中什么都不重要稀疏[i]设置为:任何一个稀疏[i]将大于n个或者它将指向稠密的这并没有指向它。不管怎样,我们都没有被愚弄。例如,假设稀疏的实际上看起来像:

Is-member公司知道忽略稀疏的成员过了那个点n个或者那样指向中的单元格稠密的这并没有指向后面,忽略灰显的条目:

注意刚才发生的事情:稀疏的可以有任意值在里面不在集合中的整数的位置,这些值实际上是在成员身份期间使用的测试,但成员资格测试表现正确!(这会驱动瓦尔格林螺母。)

可以在恒定时间内清除集合:

clear-set():n=0

归零n个有效清除稠密的(代码只能访问指数小于的密集条目n个)、和稀疏的可以未初始化,因此没有需要清除旧的价值观。

这种稀疏集表示还有一个诀窍:这个稠密的数组允许集合迭代的有效实现。

iterate():对于(i=0;i<n;i++)屈服密度[i]

让我们比较一下位向量的运行时间针对稀疏集的实现:

操作 位向量 稀疏集
是我的 O(运行)(1) O(运行)(1)
添加成员 O(运行)(1) O(运行)(1)
清除集合O(运行)()O(运行)(1)
迭代O(运行)()O(运行)(n个)

对于每次操作。唯一的问题是空间成本:两个单词替换每个位。不过,有时速度差足够了平衡增加的内存成本。Briggs和Torczon指出在编译器内部的寄存器分配期间,通常很小并且经常被清除,使得稀疏集代表选择。

稀疏集是更好选择的另一种情况是基于工作队列的图遍历算法。稀疏集上的迭代访问元素按照它们被插入的顺序(上面,5,1,4),以便在迭代期间插入新条目稍后将在同一迭代中访问。相反,位向量上的迭代访问中的元素整数顺序(1,4,5),以便插入新元素遍历期间可能会丢失,需要重复迭代。

回到原来的练习,改变是微不足道的将集合转换为向量(或矩阵)稠密的索引-值对数组,而不仅仅是索引。或者,可以将值添加到稀疏的数组或新数组。相对空间开销没有你想象的那么糟糕无论如何都要存储值。

Briggs和Torczon的论文实现了附加集操作并检查性能加速在真正的编译器中使用稀疏集。

(评论最初通过Blogger发布。)

  • 瓦哈拉苏 (2008年3月14日下午12:58)是成员(i):
    return sparse[i]<n&&密集[sparse[1]]

    这不应该吗
    return sparse[i]<n&&密集[sparse[1i]]==i
    ,因为密集可以包含空元素。显然,这只会在false=0表示时失败。

  • 俄罗斯考克斯 (2008年3月14日下午1:50)@瓦哈拉苏:已经说过了
    密集[稀疏[i]]=i;我把=改为==,所以伪代码更像C。

  • 瓦哈拉苏 (2008年3月14日下午3:13)Doh公司。块前切割的任何东西都太靠右,所以==i没有出现。从不知道div中的预块可能是这样的邪恶:)

  • 俄罗斯考克斯 (2008年3月14日下午3:43)@瓦哈拉苏:谢谢你指出这一点。添加了一些CSS魔法,使预块自动换行。

  • (2008年3月14日下午3:46)还有恒定时间移除(尽管小的更多工作):

    删除成员(i):
    如果不是-member(i):返回
    j=密实[n-1];
    稠密[稀疏[i]]=j;
    稀疏[j]=稀疏[i];
    n=n-1

  • 格鲁西姆 (2008年3月15日12:17 AM)非常酷。但是,空间要求是每个元素两个单词(我假设您的意思是2*n),这样说对吗?如果集合中的一个整数非常大,那么稀疏向量就不需要占用相应的大空间吗?

  • 俄罗斯考克斯 (2008年3月15日上午11:06)@gruseom:空间成本是每个潜在条目2个单词(每个universe成员)。我希望我已经把措辞改得更清楚了。这里的对比应该是2个单词对1位。

  • (2008年3月15日9:30 PM)从安全的角度来看,因为第一眼看到的是另一个实例,有人认为未初始化的RAM包含随机数据。实际上,这很管用,而且很酷!这是从另一个镜头看它的样子。规范、可信的“数组”是密集数组。然而,这可能访问速度较慢。因此,创建了第二个数组,其中数组中的值本身用作密集数组的索引。现在,未初始化的内存可以包含任何内容,但其中的值要么引用回受信任数据集中的条目,要么不引用。

    一个可能的麻烦来源是,如果同一个i被添加到数组中两次,第二个条目将吹走稀疏集中的第一个条目,这意味着密集集中会有一个值,而没有索引条目指向它。对于某些情况,这可能是一个问题。

    然而,一个更大的问题是,整数不需要无符号:)如果不需要,稀疏[i]处的内存可能包含一个大于MAXINT的数字,该数字将小于0,因此传递n<0的成员身份。您的系统可能会崩溃。

  • (2008年3月15日10:28 PM)除了崩溃之外,在稀疏[i]小于0的情况下,密集[sparse[i]]是未初始化的内存。所以,如果稠密[稀疏[i]]发生若要包含i,则会得到集合成员身份的假阳性。

    (当然,我假设我被限制为小于malloc要稀疏的区域的大小)

  • (2008年3月15日10:29 PM)画,

    检查n>0:)

  • 八字裤 (2008年3月15日10:44 PM)嘿Russ,
    通过一个随机链接看到了这个博客。然后我就像。。。嘿,我认识这家伙!

    很棒的博客。我已经给它添加了书签。希望你一切顺利。

    AJ公司

  • 俄罗斯考克斯 (2008年3月16日上午5:04)@是的:i是无符号的,通常add-member需要检查数字是否已经存在,所有例程都应该根据稀疏的大小检查i。

    我试图使演示保持简单。报纸上有所有血腥的细节。

  • 数据类型 (2008年3月16日上午7:30)非常酷。有一天我会用这个。

  • 莫比乌斯 (2008年3月16日上午9:27)稠密[]的元素不一定是唯一整数吗?

  • 俄罗斯考克斯 (2008年3月16日上午9:39)@莫比乌斯:是的。但集合是一种数据结构,每个元素只存储一次,所以没关系。或者,如果您根据练习要求对其进行调整,则密集包含已用数据初始化的向量索引列表。不管怎样,稠密元素的唯一性并不是一个真正的限制。

  • (2008年3月16日上午11:51)@rsc i可以全天无符号——n必须是无符号的:)问题是当您取消引用稀疏[i]时。如果该值恰好大于MAXINT,则会错过n<0检查和读取AV。

    我认为这是一个合理的假设,即我将以适合稀疏[]的方式生成。但您正在发布一篇关于如何处理内存中的任意值的帖子——您现在无法处理一半的值:)

  • (2008年3月16日下午8:46)实际上,我错了。它是稀疏[]中元素的声明,必须是无符号的。巴哈。

  • Rklz2号机组 (2008年4月7日晚上11:40)起初我以为这是一个虚拟记忆技巧。但是没有。
    嗯,实际上可能是这样。这让我想起了复制垃圾收集器,垃圾收集器也会以更多的空间使用换取更少的时间分配,而去除所有复杂性后的算法与本文中的算法具有相同的主旨。

  • 达格温 (2010年5月19日凌晨3:21)注意,所有sparse[]都必须显式初始化(无关紧要的是,最好是0),以确保陷阱表示不被访问。大多数人不会遇到这样的平台,但这是C标准允许的一种可能性。

  • 哈斯克内比 (2010年7月11日12:32 PM)这些索引技巧有很多
    被Cray程序员发现
    使用克雷散射/收集指令的。例如,您可以通过对向量(1…n)写出一个iota来确定一组整数是否唯一,然后将该集合用作分散的索引,然后将集合用作聚集的索引来读取结果。如果结果是相同的1..n,则集合是唯一的。

  • Zooko公司 (2010年7月12日上午7:50)CPython内存管理器做到了这一点,它确实推动了valgrind的发展。但幸运的是,valgrind有一个很好的特性来抑制警告,而CPython的valgrind-抑制文件会抑制这些警告。

  • 匿名(2011年1月14日上午9:32)虽然这在过去可能是真的,但当前硬件上的缓存线消耗将使位向量在任何情况下都优于此方法,除了非常小的数据集。

  • 俄罗斯考克斯 (2011年1月14日上午10:11)@anonymous:这完全取决于你清理集合并重新开始的频率。如果重置是罕见的,那么当然。然而,如果重置很常见,那么您将看到O(1)vs O(n)。当n变大时,无论你怎么看待内存系统,它都会赶上你。

  • Jean-Denis公司 (2011年2月22日凌晨1:53)链接的纸张似乎不再可用,可能除了在付费墙后面。谁能提供一个工作链接,或者可能通过电子邮件发送给我?

    谢谢,

    gmail dot com上的jdmuys

  • 莱拉 (2011年3月4日9:42 PM)你好,

    酷帖子!这是一个很巧妙的技巧,我过去也用过,走集的好处真的很好(对于许多问题,它们远比不需要初始化数组的好处重要!)但我觉得我应该稍微保护一下位向量,因为我认为它们在清除问题上受到了一些不好的评价:-)

    位向量不必是O(M)即可清除-它们可以很容易地是O(M/W)或O(M/(W*IPC)),其中W是体系结构的位宽度,IPC是每个时钟周期的指令数。

    在单问题64位体系结构上,这将是O(M/64)。。。在双问题128位SIMD体系结构(如推土机)上,这可能是O(M/256)。

    对于大于64和256的集合大小,O(M/64)和O(M/256)仍然不是O(1),但O(M/W*IPC)通常仍然比O(M)友好得多!:)

  • 匿名(2011年12月7日下午3:44)@莱拉

    这假定CPU可以发出类似的内存操作来清除这些单词。你必须考虑内存总线。x64 CPU可以通过总线上的单个指令清除256位吗?