C ++有std :: vector,Java有ArrayList,许多其他语言都有自己的动态分配数组。当动态数组空间不足时,它会重新分配到更大的区域,旧值将被复制到新数组中。这种阵列性能的核心问题是阵列的大小增长速度。如果你总是只能变得足够大以适应当前的推动,那么你每次都会重新分配。因此,将数组大小加倍,或将其乘以1.5倍是有意义的。
是否有理想的生长因子? 2倍? 1.5倍?理想上,我的意思是数学上合理,最佳平衡性能和浪费的记忆。理论上,我认识到,鉴于您的应用程序可能具有任何可能的推送分布,这在某种程度上取决于应用程序。但是我很想知道是否有一个“通常”最好的值,或者在一些严格的约束条件下被认为是最好的。
我听说有一篇论文在这个地方,但我一直都找不到。
答案 0 :(得分:88)
我记得多年前读过为什么1.5比两个更受欢迎,至少应用于C ++(这可能不适用于托管语言,运行时系统可以随意重定位对象)。
原因是:
这个想法是,在2倍扩展的情况下,没有时间点产生的漏洞足够大,可以重复用于下一次分配。使用1.5x分配,我们改为:
答案 1 :(得分:39)
完全取决于用例。您是否更关心浪费复制数据(以及重新分配数组)或额外内存的时间?阵列要持续多久?如果它不会长时间存在,使用更大的缓冲区可能是一个好主意 - 惩罚是短暂的。如果它会徘徊(例如在Java中,进入老一代和老一代),那显然更多的是惩罚。
没有“理想的增长因素”。它不仅仅是理论上依赖于应用程序,它依赖于 应用程序。
2是一个非常常见的增长因素 - 我很确定这是.NET使用的ArrayList
和List<T>
。 Java中的ArrayList<T>
使用1.5。
Dictionary<,>
使用“加倍大小然后增加到下一个素数”,以便在桶之间合理分配哈希值。 (我确信我最近看到的文档表明素数实际上并不适合分发哈希桶,但这是另一个答案的论据。)
答案 2 :(得分:38)
理想情况下(在 n →∞的限制内),it's the golden ratio:φ= 1.618 ...
在实践中,你想要一些接近的东西,比如1.5。
原因是您希望能够重用较旧的内存块,以利用缓存并避免不断使操作系统为您提供更多内存页。您要解决的等式可以确保减少到 x n - 1 - 1 = x n + 1 - x n ,其解决方案接近 x =φ表示大 n 。
答案 3 :(得分:11)
在回答这样的问题时,一种方法就是“欺骗”并看看流行的图书馆做了什么,假设一个广泛使用的图书馆至少没有做出可怕的事情。
因此,只需快速检查,Ruby(1.9.1-p129)在追加到数组时似乎使用1.5x,而Python(2.6.2)使用1.125x加上常量(在Objects/listobject.c
中):
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
PyErr_NoMemory();
return -1;
} else {
new_allocated += newsize;
}
上面的 newsize
是数组中元素的数量。请注意,newsize
被添加到new_allocated
,因此使用bitshifts和三元运算符的表达式实际上只是计算过度分配。
答案 4 :(得分:7)
假设您将数组大小增加x
。所以假设你从大小T
开始。下次生成数组时,其大小将为T*x
。然后它将是T*x^2
,依此类推。
如果您的目标是能够重用之前创建的内存,那么您需要确保分配的新内存小于您解除分配的先前内存的总和。因此,我们有这种不平等:
T*x^n <= T + T*x + T*x^2 + ... + T*x^(n-2)
我们可以从两侧移除T.所以我们得到了这个:
x^n <= 1 + x + x^2 + ... + x^(n-2)
非正式地说,我们所说的是在nth
分配时,我们希望所有先前释放的内存大于或等于第n个分配时的内存需求,以便我们可以重用以前释放的内存。 / p>
例如,如果我们希望能够在第3步(即n=3
)执行此操作,那么我们有
x^3 <= 1 + x
这个等式适用于所有x,0 < x <= 1.3
(大致)
请参阅以下不同n的x:
n maximum-x (roughly)
3 1.3
4 1.4
5 1.53
6 1.57
7 1.59
22 1.61
请注意,自2
以来,增长因素必须小于x^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2
。
答案 5 :(得分:4)
这取决于。有些人分析常见的使用案例以找到最佳数字。
我看过1.5x 2.0x phi x,之前使用的是2的功率。
答案 6 :(得分:2)
如果你有一个超过数组长度的分布,并且你有一个效用函数可以说明你有多浪费空间而不是浪费时间,那么你绝对可以选择一个最佳的大小调整(和初始大小)策略。
使用简单常数倍的原因显然是每个附加都具有摊销的常数时间。但这并不意味着您不能为小尺寸使用不同(更大)的比率。
在Scala中,您可以使用查看当前大小的函数覆盖标准库哈希表的loadFactor。奇怪的是,可调整大小的数组只增加了一倍,这是大多数人在实践中所做的事情。
我不知道任何加倍(或1.5 * ing)数组实际上会消除内存错误并且在这种情况下增长更少。看来,如果你有一个巨大的单个数组,你就是想这样做。
我还要补充一点,如果你将可调整大小的数组保持足够长的时间,并且随着时间的推移你喜欢空间,那么最初可以显着地分配(对于大多数情况)并在然后重新分配到恰当的大小时是有意义的。你已经完成了。
答案 7 :(得分:1)
我同意Jon Skeet,即使我的理论家朋友坚持认为,当将因子设置为2x时,可以证明这是O(1)。
每台机器上的CPU时间和内存之间的比例是不同的,因此因素也会有所不同。如果你有一台拥有千兆字节ram和慢速CPU的机器,那么将元素复制到一个新阵列比在快速机器上复杂得多,后者可能会减少内存。对于统一的计算机来说,这是一个理论上可以回答的问题,在实际场景中根本不能帮助你。
答案 8 :(得分:0)
我知道这是一个老问题,但有些事情似乎每个人都缺失了。
首先,这乘以2:size&lt;&lt; 1.这是乘以任何在1和2之间:int(float(size)* x),其中x是数字,*是浮点数学,处理器必须运行其他指令用于在float和int之间进行转换。换句话说,在机器级别,加倍需要一个非常快速的指令来查找新的大小。乘以1到2之间的东西需要至少一条指令来将大小转换为浮点数,一条指令要相乘(这是浮点乘法,所以它可能需要至少两倍的周期,如果不是4或者甚至是8倍,以及一个返回int的指令,并假设您的平台可以在通用寄存器上执行浮点运算,而不需要使用特殊寄存器。简而言之,您应该期望每次分配的数学运算至少是简单左移的10倍。如果您在重新分配期间复制了大量数据,那么这可能没什么区别。
其次,可能是最大的挑战者:每个人似乎都认为被释放的内存既与自身连续,又与新分配的内存相邻。除非您自己预先分配所有内存然后将其用作池,否则几乎肯定不是这样。 OS 可能偶尔最终会这样做,但大多数时候,会有足够的可用空间碎片,任何一半体面的内存管理系统都能找到一个小洞,你的记忆将会恰到好处。一旦你真的有点块,你更有可能最终得到连续的部分,但到那时,你的分配足够大,你不会经常做足够的事情,因为它不再重要。简而言之,想象使用一些理想数字可以最有效地利用可用内存空间是很有趣的,但实际上,除非你的程序是在裸机上运行,否则它不会发生(例如,没有操作系统)在它下面作出所有决定。)
我对这个问题的回答?不,没有理想的数字。这是特定应用程序,没有人真正尝试过。如果你的目标是理想的内存使用,那你几乎没有运气。对于性能,较少频繁的分配更好,但如果我们只是这样,我们可以乘以4甚至8!当然,当Firefox一次性从1GB跳到8GB时,人们会抱怨,所以这甚至没有意义。以下是一些经验法则:
如果无法优化内存使用,至少不要浪费处理器周期。乘以2至少比进行浮点数学运算快一个数量级。它可能不会产生巨大的差异,但它至少会产生一些影响(尤其是在更频繁和更小的分配期间的早期)。
不要过度思考它。如果你只花了4个小时试图弄清楚如何做已经完成的事情,那你就浪费了你的时间。说实话,如果有比* 2更好的选择,那么几十年前就可以在C ++矢量类(以及许多其他地方)中完成。
最后,如果真的想要优化,请不要为这些小东西烦恼。现在几天,没有人关心浪费4KB的内存,除非他们正在使用嵌入式系统。当你得到1GB的每个介于1MB和10MB之间的对象时,加倍可能是太多了(我的意思是,这是100到1,000个对象之间)。如果您可以估算预期的扩张率,您可以将其调整到某个点的线性增长率。如果你希望每分钟大约有10个物体,那么每步增加5到10个物体尺寸(每30秒一分钟一次)可能就好了。
归根结底是,不要过度思考,优化您的能力,并根据您的需要自定义您的应用程序(和平台)。
答案 9 :(得分:0)
另外两美分
old*3/2
计算,因此不需要浮点运算。 (我说/2
因为如果他们认为合适,编译器会在生成的汇编代码中用位移代替它。)正如有人提到的那样2感觉好于8.而且2感觉好于1.1。
我的感觉是1.5是一个很好的默认值。除此之外,它取决于具体情况。
答案 10 :(得分:0)
最高投票和接受的答案都很好,但都没有回答问题中要求“数学上合理的”“理想增长率”、“最佳平衡性能和浪费内存”的部分。 (票数第二的答案确实试图回答这部分问题,但其推理令人困惑。)
这个问题完美地指出了必须平衡的 2 个注意事项,性能和浪费的内存。如果您选择的增长率太低,则性能会受到影响,因为您会过快地用完额外空间并且不得不过于频繁地重新分配。如果您选择的增长率太高,例如 2 倍,则会浪费内存,因为您永远无法重用旧的内存块。
特别是,如果你 do the math1 你会发现增长率的上限是黄金比例 ϕ = 1.618… 。大于 ϕ 的增长率(如 2x)意味着您将永远无法重用旧的内存块。仅略低于 ϕ 的增长率意味着您将无法重用旧内存块,直到经过多次重新分配,在此期间您将浪费内存。因此,您希望在不牺牲太多性能的情况下尽可能低于 ϕ。
因此,我建议将这些候选对象用于“数学上合理的”“理想增长率”、“最佳平衡性能和浪费的内存”:
很明显,那里有一些收益递减,所以我认为全局最优解可能就是其中之一。另外,请注意,无论全局最优值实际上是什么,1.5x 都是一个很好的近似值,并且具有非常简单的优点。
1感谢@user541686 这个优秀的来源。