![]() |
1
155
想到的一个简单的可能性是,对于常见情况,每个值保留一个2位的压缩数组,每个值保留一个单独的4字节(原始元素索引为24位,实际值为8位,因此
查找值时,首先在2bpp数组中进行查找(O(1));如果您找到0、1或2,这就是您想要的值;如果找到3,则意味着必须在辅助数组中查找它。在这里,您将执行二进制搜索以查找 指数 将感兴趣的值左移8(O(log(n)加上一个小的n,因为这应该是1%),然后从4字节的内容中提取值。
对于您提议的数组,第一个数组需要10000000/4=2500000字节,第二个数组需要10000000*1%*4 B=400000字节;因此,2900000字节,即不到原始数组的三分之一,而使用最多的部分都保存在内存中,这应该有利于缓存(甚至可能适合L3)。 如果您需要超过24位的寻址,则必须调整“辅助存储”;扩展它的一个简单方法是使用256个元素的指针数组来切换索引的前8位,并转发到如上所述的24位索引排序数组。 快速基准测试
(code and data always updated in my Bitbucket) 上面的代码使用随机数据填充一个10M元素数组,这些数据按照帖子中指定的OP分布,初始化我的数据结构,然后:
(请注意,在顺序查找的情况下,数组总是以巨大的优势获胜,因为它是您可以执行的最有利于缓存的查找) 最后两个块重复50次并计时;最后,计算并打印每种查找类型的平均值和标准偏差,以及加速比(lookup\u mean/array\u mean)。
我用g++5.4.0编译了上面的代码(
结果是。。。混血儿!
|
![]() |
2
33
另一种选择可能是
换句话说,类似于:
哪里
这个结构更新起来很简单,占用了25%以上的内存,但大部分只在5%的情况下查找。当然,像往常一样,这是否是一个好主意取决于许多其他条件,因此唯一的答案是尝试实际使用。 |
![]() |
3
23
这与其说是一个具体的答案,不如说是一个“冗长的评论” 除非你的数据是众所周知的,否则我怀疑任何人都不能直接回答你的问题(我不知道有什么与你的描述相匹配,但我也不知道所有用例的所有数据模式)。稀疏数据是高性能计算中的常见问题,但它通常是“我们有一个非常大的数组,但只有一些值是非零的”。 对于像我认为你的模式这样不为人所知的模式,没有人会直接知道哪个更好,这取决于细节:随机访问的随机性如何-系统是访问数据项的集群,还是完全随机的,就像从统一的随机数生成器来的一样。表格数据是完全随机的,还是有0的序列,然后是1的序列,还有其他值的分散?如果您有相当长的0和1序列,则运行长度编码可以很好地工作,但如果您有“0/1棋盘”,则运行长度编码将不起作用。此外,您还必须保留一个“起点”表,以便您能够以合理的速度到达相关地点。 我很久以前就知道,一些大型数据库只是RAM中的一个大表(本例中是电话交换机用户数据),其中的一个问题是处理器中的缓存和页表优化非常无用。调用方很少与最近调用某人的调用方相同,因此没有任何类型的预加载数据,它只是纯粹的随机数据。对于这种访问类型,大页面表是最好的优化。 在很多情况下,在“速度和小尺寸”之间进行折衷是软件工程中必须选择的事情之一[在其他工程中,这不一定是一种折衷]。因此,“为更简单的代码浪费内存”通常是首选。从这个意义上说,“简单”的解决方案很可能在速度上更好,但如果您对RAM有“更好”的使用,那么优化表的大小将为您提供足够的性能和大小上的良好改进。有很多不同的方法可以实现这一点-正如在注释中所建议的那样,一个2位字段存储两个或三个最常见的值,然后是其他值的一些替代数据格式-哈希表是我的第一种方法,但列表或二叉树也可能起作用-同样,这取决于“不是0、1或2”的模式。同样,这取决于值在表中是如何“分散”的-它们是在集群中还是更均匀地分布? 但问题是,您仍然在从RAM读取数据。然后,您将花费更多的代码来处理数据,包括一些代码来处理“这不是一个常见值”。 最常见的压缩算法的问题是,它们基于解包序列,因此不能随机访问它们。而且,将大数据分割成块(例如,一次256个条目),然后将256个条目解压缩到uint8\t数组中,获取所需数据,然后丢弃未压缩的数据,这样的开销不太可能给您带来好的性能—当然,假设这很重要。 最后,您可能需要实现注释/答案中的一个或几个想法来测试,看看它是否有助于解决您的问题,或者内存总线是否仍然是主要的限制因素。 |
![]() |
4
13
我过去所做的是在 正面 位集的。 与Matteo的答案相比,这将占用一半的空间,但如果“异常”查找速度较慢(即存在许多异常),则可能会更慢。 然而,通常情况下,“缓存为王”。 |
![]() |
5
11
除非你的数据有规律,否则就不可能有任何合理的速度或大小优化,而且——假设你的目标是一台普通计算机——10 MB也没什么大不了的。 你的问题有两个假设:
我认为这两个假设都是错误的。在大多数情况下,存储数据的适当方式是存储最自然的表示。在您的情况下,这就是您想要的:一个字节表示0到255之间的数字。任何其他表示都将更加复杂,因此,在所有其他条件相同的情况下,速度较慢,更容易出错。要想偏离这一一般原则,您需要一个比95%的数据上可能存在的六个“浪费”位更有说服力的理由。 对于第二个假设,如果且仅当更改数组大小导致缓存未命中大幅减少时,才是正确的。这是否会发生只能通过分析工作代码来确定,但我认为这不太可能产生实质性的影响。因为在这两种情况下,您都将随机访问阵列,因此处理器将很难知道在这两种情况下要缓存和保留哪些数据位。 |
![]() |
6
8
如果数据和访问是均匀随机分布的,那么性能可能取决于访问中避免外部级别缓存丢失的部分。优化这一点需要知道缓存中可以可靠容纳的阵列大小。如果缓存足够大,每五个单元可以容纳一个字节,最简单的方法可能是让一个字节保存0-2范围内的五个基三编码值(有243个5个值的组合,因此可以放入一个字节),以及一个10000000字节数组,只要基三值指示“2”,就会查询该数组。 如果缓存没有那么大,但每8个单元可以容纳一个字节,那么就不可能使用一个字节的值从所有6561个可能的八个基3值组合中进行选择,但由于将0或1更改为2的唯一效果是导致不必要的查找,因此正确性不需要支持所有6561。相反,我们可以关注256个最“有用”的值。 特别是如果0比1更常见,或者反之亦然,一个好的方法可能是使用217个值对包含5个或更少1的0和1的组合进行编码,16个值对xxxx0000到xxxx1111进行编码,16个值对0000xxxx到1111xxxx进行编码,一个值对xxxxxxxx进行编码。四个值将保留下来,以供人们可能发现的任何其他用途。如果数据按所述随机分布,则所有查询中的绝大多数都会命中仅包含0和1的字节(在所有8个组中的约2/3中,所有位都是0和1,其中约7/8的字节具有6个或更少的1位);绝大多数没有这样做的人会在一个包含四个x的字节中着陆,并且有50%的几率会在0或1上着陆。因此,大约四分之一的查询需要进行大型数组查找。 如果数据是随机分布的,但缓存的大小不足以每八个元素处理一个字节,那么可以尝试使用这种方法,每个字节处理八个以上的项,但除非对0或1有强烈的偏向,否则可以在不必在大数组中查找的情况下处理的值的分数将随着每个字节处理的数量的增加而减少。 |
![]() |
7
7
我将添加到 @o11c 的回答,因为他的措辞可能有点混乱。 如果需要压缩最后一位和CPU周期,我会执行以下操作。 我们将从构建 平衡的 二元搜索树,包含5%的“其他”案例。对于每个查找,您都可以快速遍历树:您有10000000个元素:其中5%在树中:因此树数据结构包含500000个元素。在O(log(n))时间内遍历此过程,将得到19次迭代。我不是这方面的专家,但我想还有一些内存高效的实现。让我们猜测一下:
总计4个字节:500000*4=1953 kB。适合缓存! 对于所有其他情况(0或1),可以使用位向量。请注意,对于随机访问,您不能忽略5%的其他情况:1.19 MB。 这两者的结合使用了大约3099 MB。使用此技术,您将节省3.08倍的内存。 然而,这并没有击败 @意大利马特奥 (使用2.76 MB),很遗憾。我们还有什么可以做的吗?最消耗内存的部分是树中索引的3个字节。如果我们能将这个值降到2,我们将节省488kB,总内存使用量将是:2.622MB,这是更小的!
我们如何做到这一点?我们必须将索引减少到2个字节。同样,10000000需要23位。我们需要能够删除7位。我们可以简单地将10000000个元素的范围划分为78125个元素的2^7(=128)个区域。现在,我们可以为每个区域构建一个平衡树,平均包含3906个元素。只需将目标索引除以2^7(或一个位移位),即可选择正确的树
请注意,从理论上讲,您可以重复此过程以切断接下来的8位,但这需要您创建2^15个平衡树,平均约305个元素。这将导致2.143MB的内存,只需4次迭代即可遍历树,与我们开始的19次迭代相比,这是一个相当大的加速。 最后得出的结论是:这比2位向量策略的内存使用量要小,但要实现起来却很困难。但是,如果它能够区分是否适合缓存,那么可能值得一试。 |
![]() |
8
5
如果只执行读取操作,最好不要将值赋给单个索引,而是赋给索引的间隔。 例如:
这可以通过结构来完成。如果您喜欢OO方法,还可能需要定义类似于此的类。
现在,您只需通过一个间隔列表进行迭代,并检查您的索引是否位于其中一个间隔内,平均而言,该间隔占用的内存更少,但占用的CPU资源更多。
如果按大小降序排列时间间隔,则会增加提前找到要查找的项目的可能性,从而进一步降低平均内存和CPU资源使用率。 您还可以删除大小为1的所有间隔。将相应的值放入映射中,仅当在间隔中找不到要查找的项目时才进行检查。这也应该会稍微提高平均性能。 |
![]() |
9
4
很久很久以前,我只记得。。。 在大学里,我们的任务是加速光线跟踪器程序,该程序必须通过算法从缓冲区阵列中反复读取。一位朋友告诉我要始终使用4字节倍数的RAM读取。因此,我将阵列的模式从[x1,y1,z1,x2,y2,z2,…,xn,yn,zn]更改为[x1,y1,z1,0,x2,y2,z2,0,…,xn,yn,zn,0]。意味着在每个三维坐标后添加一个空字段。经过一些性能测试:速度更快。 长话短说:从RAM中读取数组中4个字节的倍数,也可以从正确的起始位置读取,因此您可以在其中读取一个小簇,其中包含搜索到的索引,并从cpu中的这个小簇中读取搜索到的索引。(在您的情况下,不需要插入填充字段,但概念应该明确) 也许其他倍数也可能是新系统的关键。 我不知道这是否适用于你的情况,所以如果不适用:对不起。如果成功的话,我很高兴听到一些测试结果。 PS:哦,如果有任何访问模式或附近的访问索引,您可以重用缓存的集群。 PPS:可能是,倍数更像是16字节之类的,那是很久以前的事了,我能准确地记住。 |
![]() |
10
3
考虑到这一点,您可以拆分数据,例如:
在这种情况下,所有值都会一直显示到给定的索引,因此您甚至可以删除其中一个位集,并将该值表示为在其他位集中丢失的值。 这将为这种情况节省一些内存,但会使最坏的情况变得更糟。 您还需要更多的CPU能力来进行查找。 确保测量! |
![]() |
11
2
就像Mats在评论回答中提到的那样,如果不知道什么才是最好的解决方案,很难说出来 明确地 您拥有什么样的数据(例如,是否有长时间运行的0等),以及您的访问模式是什么样的(“随机”是指“到处都是”还是“不完全以线性方式”或“每个值都只随机一次”或…)。 这就是说,我们想到了两种机制:
|
![]() |
12
1
我对C不是很熟悉,但在 C类++ 您可以使用 无符号字符 表示0-255范围内的整数。 与正常值相比 内景 (再一次,我来自 JAVA 和 C类++ 世界)其中 4字节 (32位)是必需的 无符号字符 需要 1字节 (8位)。 因此,它可能会将阵列的总大小减少75%。 |
![]() |
13
-4
您已经简洁地描述了数组的所有分布特征; 投掷阵列 。 您可以轻松地使用随机方法替换数组,该方法生成与数组相同的概率输出。 如果一致性很重要(为相同的随机索引生成相同的值),请考虑使用 bloom filter 和/或 hash map 跟踪重复点击。但是,如果您的阵列访问确实是随机的,那么这完全没有必要。 |