Spikes in array capacity

Ali Çehreli acehreli at yahoo.com
Fri Jul 2 07:01:11 PDT 2010


Steven Schveighoffer wrote:
 > On Fri, 02 Jul 2010 01:34:26 -0400, Ali Çehreli <acehreli at yahoo.com> 
wrote:
 >
 >> BCS wrote:
 >>
 >>  >> There is something strange in the array capacity algorithm.
 >>  >> [...]
 >>  >> Is that intended?
 >>
 >>  > I'm going to take a guess that the gc is (after some point) 
allocating
 >>  > into the smallest hole with "enough" capacity.
 >>
 >> You may be right. :)
 >>
 >>  > If you re run the test
 >>  > but with something else going on to add some noise, do the spikes
 >> move?
 >>
 >> Yes, the spikes move.
 >>
 >>  > void makeNoise()
 >>  > {
 >>  >      static byte[][256] data;
 >>  >      data[rand() % $] = new byte[rand() % 512];
 >>  > }
 >>
 >> I am not familiar with the dmd internals; but following the code took
 >> me to the function newCapacity() in src/druntime/src/rt/lifetime.d.
 >> The capacity growth is more complicated than what I've been assuming
 >> so far.
 >>
 >> And yes, GC is in the picture. Quoting the comments from that file:
 >>
 >> /*
 >>   * Better version by Dave Fladebo:
 >>   * This uses an inverse logorithmic algorithm to pre-allocate a bit 
more
 >>   * space for larger arrays.
 >>   * - Arrays smaller than PAGESIZE bytes are left as-is, so for the most
 >>   * common cases, memory allocation is 1 to 1. The small overhead added
 >>   * doesn't affect small array perf. (it's virtually the same as
 >>   * current).
 >>   * - Larger arrays have some space pre-allocated.
 >>   * - As the arrays grow, the relative pre-allocated space shrinks.
 >>   * - The logorithmic algorithm allocates relatively more space for
 >>   * mid-size arrays, making it very fast for medium arrays (for
 >>   * mid-to-large arrays, this turns out to be quite a bit faster 
than the
 >>   * equivalent realloc() code in C, on Linux at least. Small arrays are
 >>   * just as fast as GCC).
 >>   * - Perhaps most importantly, overall memory usage and stress on 
the GC
 >>   * is decreased significantly for demanding environments.
 >>   */
 >>
 >> So there may not be a bug after all...
 >>
 >> Ali
 >
 > If your original code is all that was being run, I think there is a bug.

The original code was it.

 > I'll tell you why -- the GC provides a way to append pages to an
 > allocated block.  Essentially, you can glue consecutive unused pages
 > together to make a larger block, thereby growing your block.
 >
 > But anything under a page is simply reallocated because anything less
 > than a page is inside a pool of blocks of the same size, and those
 > cannot be glued together.
 >
 > So when your array is growing, and it gets larger than a page, it should
 > grow a single page at a time.  But I think (and I stress think, I'm not
 > sure) that when a block of pages is deallocated, each page becomes
 > independently free.  They do not stay glued together.  So for an
 > allocating requesting 1 or 2 more pages to grow by 2000 pages is
 > suspect.  I still have to examine the code, but I think there is a 
problem.
 >
 > Back to my original assertion -- to disprove the theory that some other
 > "larger hole" is why it gets so big, in one instance your number of
 > elements grows from 1,155,070 (4MB) to 3,153,917 (12MB), that's almost a
 > 200% increase.  In order for that 12MB hole to exist, something must
 > have allocated it before, and then deallocated it.  It can't be your
 > loop because your loop has not allocated that much space yet.  I would
 > find that very strange to happen somewhere in the runtime without you
 > specifically allocating it.  If however, your original post is not the
 > entire code, then there may be some possibility to that.
 >
 > BTW, you can take the newCapacity function out of the loop by simply
 > setting the length and then writing the last element, i.e.:
 >
 > a.length += 1;
 > a[$-1] = i;
 >
 > newCapacity isn't called when setting the length explicitly, because
 > it's not considered to be an append.

You are right.

Looks like I was successful in locating the culprit. :) I took liberty 
to write ++a.length and also outputted the number of elements that are 
in reserve at each allocation:

import std.stdio;
import std.array;

void main()
{
     int[] a;
     size_t oldCapacity;

     foreach (i; 0 .. 100_000_000) {
         ++a.length;
         a[$-1] = i;

         if (capacity(a) != oldCapacity) {
             writeln("length: ", a.length,
                     " capacity: ", capacity(a),
                     " ratio: ", cast(double)capacity(a) / oldCapacity,
                     " reserve: ", capacity(a) - a.length);
             oldCapacity = capacity(a);
         }
     }
}

Now the spikes are gone, and the allocations asymptote at once-per-1024 
elements for an int array:

length: 1 capacity: 3 ratio: inf reserve: 2
length: 4 capacity: 7 ratio: 2.33333 reserve: 3
length: 8 capacity: 15 ratio: 2.14286 reserve: 7
length: 16 capacity: 31 ratio: 2.06667 reserve: 15
length: 32 capacity: 63 ratio: 2.03226 reserve: 31
length: 64 capacity: 127 ratio: 2.01587 reserve: 63
length: 128 capacity: 255 ratio: 2.00787 reserve: 127
length: 256 capacity: 509 ratio: 1.99608 reserve: 253
length: 512 capacity: 1021 ratio: 2.00589 reserve: 509
length: 1022 capacity: 2045 ratio: 2.00294 reserve: 1023
length: 2046 capacity: 3069 ratio: 1.50073 reserve: 1023
length: 3070 capacity: 4093 ratio: 1.33366 reserve: 1023
...

In the long run, there is one allocation per 1024 elements. That still 
feels like amortized constant, but considering that relocation times 
would be O(N), I think reserve should still grow with N, but maybe not 
too much. (I am not sure about this last point at all.)

 > -Steve

Thank you very much,
Ali


More information about the Digitalmars-d mailing list