views:

373

answers:

6

C++ has std::vector and Java has ArrayList, and many other languages have their own form of dynamically allocated array. When a dynamic array runs out of space, it gets reallocated into a larger area and the old values are copied into the new array. A question central to the performance of such an array is how fast the array grows in size. If you always only grow large enough to fit the current push, you'll end up reallocating every time. So it makes sense to double the array size, or multiply it by say 1.5x.

Is there an ideal growth factor? 2x? 1.5x? By ideal I mean mathematically justified, best balancing performance and wasted memory. I realize that theoretically, given that your application could have any potential distribution of pushes that this is somewhat application dependent. But I'm curious to know if there's a value that's "usually" best, or is considered best within some rigorous constraint.

I've heard there's a paper on this somewhere, but I've been unable to find it.

+4  A: 

It will entirely depend on the use case. Do you care more about the time wasted copying data around (and reallocating arrays) or the extra memory? How long is the array going to last? If it's not going to be around for long, using a bigger buffer may well be a good idea - the penalty is short-lived. If it's going to hang around (e.g. in Java, going into older and older generations) that's obviously more of a penalty.

There's no such thing as an "ideal growth factor." It's not just theoretically application dependent, it's definitely application dependent.

2 is a pretty common growth factor - I'm pretty sure that's what ArrayList and List<T> in .NET uses. ArrayList<T> in Java uses 1.5.

EDIT: As Erich points out, Dictionary<,> in .NET uses "double the size then increase to the next prime number" so that hash values can be distributed reasonably between buckets. (I'm sure I've recently seen documentation suggesting that primes aren't actually that great for distributing hash buckets, but that's an argument for another answer.)

Jon Skeet
For simple arrays, 2 is used. For the backing data of Dictionary, the prime number larger than 2*size is used. Just a strange edge case where growth is not a linear factor. (But I am sure you already knew that, actually). Just thought others would find that interesting.
Erich Mirabal
Yup, will edit.
Jon Skeet
When I was thinking of a theoretical ideal I was imagining it would be given certain constraints that the authors of some paper would think were realistic for most applications, like assuming that the size of the allocations or their frequency fits a bell curve or something along those lines.
Joseph Garvin
+1  A: 

I agree with Jon Skeet, even my theorycrafter friend insists that this can be proven to be O(1) when setting the factor to 2x.

The ratio between cpu time and memory is different on each machine, and so the factor will vary just as much. If you have a machine with gigabytes of ram, and a slow CPU, copying the elements to a new array is a lot more expensive than on a fast machine, which might in turn have less memory. It's a question that can be answered in theory, for a uniform computer, which in real scenarios doesnt help you at all.

Tom
+5  A: 

I remember reading many years ago why 1.5 is preferred over two, at least as applied to C++ (this probably doesn't apply to managed languages, where the runtime system can relocate objects at will).

The reasoning is this:

  1. Say you start with a 16-byte allocation.
  2. When you need more, you allocate 32 bytes, then free up 16 bytes. This leaves a 16-byte hole in memory.
  3. When you need more, you allocate 64 bytes, freeing up the 32 bytes. This leaves a 48-byte hole (if the 16 and 32 were adjacent).
  4. When you need more, you allocate 128 bytes, freeing up the 32 bytes. This leaves a 112-byte hole (assuming all previous allocations are adjacent).
  5. And so and and so forth.

The idea is that, with a 2x expansion, there is no point in time that the resulting hole is ever going to be large enough to reuse for the next allocation. Using a 1.5x allocation, we have this instead:

  1. Start with 16 bytes.
  2. When you need more, allocate 24 bytes, then free up the 16, leaving a 16-byte hole.
  3. When you need more, allocate 36 bytes, then free up the 24, leaving a 40-byte hole.
  4. When you need more, allocate 54 bytes, then free up the 36, leaving a 76-byte hole.
  5. When you need more, allocate 81 bytes, then free up the 54, leaving a 130-byte hole.
  6. When you need more, use 122 bytes (rounding up) from the 130-byte hole.
Chris Jester-Young
A random forum post I found (http://objectmix.com/c/129049-can-array-allocation-cause-memory-fragmentation.html) reasons similarly. A poster claims that (1+sqrt(5))/2 is the upper limit for reuse.
Naaff
If that claim is correct, then phi (== (1 + sqrt(5)) / 2) is indeed the optimal number to use.
Chris Jester-Young
I like this answer because it reveals the rationale of 1.5x versus 2x, but Jon's is technically most correct for the way I stated it. I should have just asked why 1.5 has been recommended in the past :p
Joseph Garvin
+2  A: 

It really depends. Some people analyze common usage cases to find the optimal number.

I've seen 1.5x 2.0x phi x, and power of 2 used before.

Unknown
Phi! That's a nice number to use. I should start using it from now on. Thanks! +1
Chris Jester-Young
I don't understand...why phi? What properties does it have that makes it suitable for this?
Jason Creighton
@Jason: phi makes for a Fibonacci sequence, so the next allocation size is the sum of the current size and the previous size. This allows for moderate rate of growth, faster than 1.5 but not 2 (see my post as to why >= 2 is not a good idea, at least for unmanaged languages).
Chris Jester-Young
@Jason: Also, according to a commenter to my post, any number > phi is in fact a bad idea. I haven't done the math myself to confirm this, so take it with a grain of salt.
Chris Jester-Young
+2  A: 

One approach when answering questions like this is to just "cheat" and look at what popular libraries do, under the assumption that a widely used library is, at the very least, not doing something horrible.

So just checking very quickly, Ruby (1.9.1-p129) appears to use 1.5x when appending to an array, and Python (2.6.2) uses 1.125x plus a constant: (in 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 above is the number of elements in the array. Note well that newsize is added to new_allocated, so the expression with the bitshifts and ternary operator is really just calculating the over-allocation.

Jason Creighton
So it grows the array from n to n + (n/8 + (n<9?3:6)), which means the growth factor, in the question's terminology, is 1.25x (plus a constant).
ShreevatsaR
Wouldn't it be 1.125x plus a constant?
Jason Creighton
Er right, 1/8=0.125. My mistake.
ShreevatsaR
+1  A: 

If you have a distribution over array lengths, and you have a utility function that says how much you like wasting space vs. wasting time, then you can definitely choose an optimal resizing (and initial sizing) strategy.

The reason the simple constant multiple is used, is obviously so that each append has amortized constant time. But that doesn't mean you can't use a different (larger) ratio for small sizes.

In Scala, you can override loadFactor for the standard library hash tables with a function that looks at the current size. Oddly, the resizable arrays just double, which is what most people do in practice.

I don't know of any doubling (or 1.5*ing) arrays that actually catch out of memory errors and grow less in that case. It seems that if you had a huge single array, you'd want to do that.

I'd further add that if you're keeping the resizable arrays around long enough, and you favor space over time, it might make sense to dramatically overallocate (for most cases) initially and then reallocate to exactly the right size when you're done.

wrang-wrang