Can assumeSafeAppend() grab more and more capacity?

Biotronic via Digitalmars-d-learn digitalmars-d-learn at puremagic.com
Wed Jun 7 00:56:55 PDT 2017


On Wednesday, 7 June 2017 at 05:43:06 UTC, ag0aep6g wrote:
> [snip]

It seems to me this is a topic worthy of a more in-depth article. 
If only I felt up to that. :p

When you create a slice 'a' in D (with the current GC and 
druntime, at least), what happens behind the scenes is the 
allocator chops off a block of N bytes, where N is some number 
larger than a.length*typeof(a[0]).sizeof. For an array of two 
ints, N is 16.
For good measure, let's make a copy 'b' of that slice (it will 
come in handy later):

int[] a = [1, 2];
int[] b = a;

import std.stdio;
writeln(a.capacity);
> 3
writeln(b.capacity);
> 3

The capacity is 3. Intriguing, as a block of 16 bytes should be 
able to hold 4 ints.

We can ask the GC for more info on this block:

import core.memory;
auto info = GC.query(a.ptr);
writefln("0x%x, %s, %s ", info.base, info.size, info.attr);
> 0x2211010, 16, 10

That's the pointer to the start of the block, the size of the 
block, and various attributes (appendable, e.g.).
We can get the raw data of the block:

auto block = (cast(ubyte*)info.base)[0..info.size];
writeln(block);
> [1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8]

We can see our 1 and 2 in there, and a curious 8 at the end. 
That's the currently used data, in bytes. That's also the reason 
the capacity is 3, not 4 - this info has to live somewhere. If we 
were to append another element, and print the data again:

a ~= 3;
writeln(block);
> [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 12]

See how the last byte changed to a 12? That just so happens to be 
a.length*int.sizeof.

Remember how we made a copy of the slice above? The copy's 
capacity is now 0, while a's capacity is 3. The algorithm for 
capacity is actually pretty simple:

int capacity;
if (a.length*int.sizeof == block[$-1])
     capacity = (block.length - 1) / int.sizeof;
else
     capacity = 0;
writeln(capacity);
> 3

What happens when we call assumeSafeAppend?

b.assumeSafeAppend;
writeln(block);
> [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 8]

Hey, the 'used' byte is 8 again. That means a.capacity is 0, 
while b.capacity is 3.

Now for a curious thing: what happens to a's capacity when we 
append to b?

b ~= 4;
writeln(a.capacity);
> 3

As above, the length of a in bytes equals the used bytes in the 
allocated memory block, and so both a and b have capacity again.

This has of course overwritten a[2], which used to be 3 and is 
now 4. assumeSafeAppend breaks part of D's type system for 
optimization purposes, and this is the result.

Note that the details in this post are only correct for small 
blocks (<=256 bytes). For larger blocks, the 'used' field is 
larger, but the algorithms and concepts are the same.

For the actual implementation of a.capacity, you can have a 
look-see at 
https://github.com/dlang/druntime/blob/master/src/rt/lifetime.d#L734, which is called from https://github.com/dlang/druntime/blob/master/src/object.d#L2968.

--
   Biotronic


More information about the Digitalmars-d-learn mailing list