Request for comments: std.d.lexer

Dmitry Olshansky dmitry.olsh at gmail.com
Sat Feb 2 13:54:59 PST 2013


02-Feb-2013 01:10, Walter Bright пишет:
> On 2/1/2013 3:22 AM, Dmitry Olshansky wrote:
>> 01-Feb-2013 15:05, Walter Bright пишет:
>>> On 1/30/2013 8:44 AM, Dmitry Olshansky wrote:
>>>> In allocation scheme I proposed that ID could be a 32bit offset into
>>>> the unique
>>>> identifiers chunk.
>>>
>>> That only works if you know in advance the max size the chunk can ever
>>> be and preallocate it. Otherwise, you have no guarantee that the next
>>> allocated chunk will be within 32 bits of address of the previous
>>> chunks.
>>>
>>
>> Well I supposed it's exactly one reallocatable block. Then token have
>> an offset
>> that doesn't care if the block was reallocated.
>>
>> Or rather the reallocating just RESERVE virtual RAM for it (say 1G),
>> and COMMIT
>> it page by page when you need to grow it. Once lexing is done, shrink
>> virtual
>> region to the actual used size to free up address space (e.g. if we
>> are on 32bits).
>>
>> AS for 32bit limit that gives 4Gb maximum of the cumulative length of
>> all unique
>> identifier names is more then enough by any standard. I haven't seen a 4G
>> codebase not to speak of identifiers alone that even if we count all the
>> repetitions separately.
>
> Your technique can work, provided the number of identifiers isn't large
> enough that memory fragmentation will prevent being able to reallocate
> the buffer to a larger size.
>

I think I failed to describe the virtual memory trick or you just 
ignored it? But regardless I have to correct myself on both the amount 
to reserve and the fact that it's actually not virtual memory alone but 
a virtue of MMU.

See below a full program in D see it the trick in action, currently 
Win32 only, as I can't recall the right POSIX calls offhand. It's a 
benchmark against built-in array/appender - the heap stuff.

The speed is simillar, with my quick and dirty stuff being much faster 
iff arrays don't allocate all of ram beforehand.

The steps to snatch the power of not having any reallocations:

1. Reserve a contiguous *address space* range. Not even a bit of virtual 
memory is reserved/commited/touched at this moment. This just creates a 
TLB entry AFAICT.

(for lexer this address space range would have length equal to the sum 
of all files length as Tove points out, this is the first correction)

2. Any time there is no memory to put more stuff into (as is initially), 
commit the next few pages. At this moment virtual ram is committed but 
not allocated.

3. [this I think we all know] The moment memory location in the 
committed part of the range is touched the page for it is allocated (and 
on win32 initially contains zeros automatically). Only at this point 
Virtual memory is allocated

So you see where my 2nd correction goes. In essence you get: 0 
reallocations and no more then 1 page of virtual memory wasted in all 
cases. Sweet deal ;)

Then there is an optional step if you think that too much of address 
space is used (makes sense only on 32-bits if at all): copy the 
resulting block of actual data elsewhere in the heap and release the 
whole address range.

The code:
(the same as github gist: https://gist.github.com/4699388 )

///Written in the D programming language

// Bench built-in array append, std.array.Appender
// and custom virtual-memory aware VirtualArray in terms of
// memory usage and the time taken
// to append up to X megabytes using different chunk sizes:
// 16 bytes, 64bytes, 256 bytes and 16Kb at a time.


// pass this version to take insert readln's
// at interesting points and investigate RAM usage
// make sure to enable relevant columns in task manager process details:
// committed size and private working set
// version = diagnose;
import std.c.windows.windows;

import std.exception, std.datetime, std.algorithm, std.range, std.array;

auto roundUpToPowerOf2(size_t var, size_t pow2)
{
      return (var + pow2-1) & ~(pow2-1);
}

struct VirtualArray
{
private:
     ubyte* pointer;     // to the beginning of the reserved memory
     size_t _length;     // length of data
     size_t commited;   // committed memory so far
     size_t reserved;   // total possible maximum to grow to
     size_t pageSize;   // page size, this could be global
     //size_t pageSize;

     // commit some more memory from the reserved range
     void extend(size_t size)
     {
         size = roundUpToPowerOf2(size, pageSize);
         // this will throw once run out of reserved pages
         enforce(VirtualAlloc(pointer+commited, size,
             MEM_COMMIT, PAGE_READWRITE));
         commited += size;
     }
public:
     this(size_t maxCapacity)
     {
         SYSTEM_INFO sysinfo;
         GetSystemInfo(&sysinfo);
         pageSize = sysinfo.dwPageSize;
         // the page size is a power of 2
         // round the capacity up to a multiple of page size
         maxCapacity = roundUpToPowerOf2(maxCapacity, pageSize);
         pointer = cast(ubyte*)enforce(VirtualAlloc(null, maxCapacity,
             MEM_RESERVE, PAGE_READWRITE));
         commited = 0;
         reserved = maxCapacity;
         _length = 0;
     }

     // bare minimum primitives to run benchmark
     ref ubyte opIndex(size_t idx)
     {
         assert(idx < length);
         return pointer[idx];
     }

     // ditto
     ubyte[] opSlice(size_t from, size_t to)
     {
         assert(from < to && to <= length);
         return pointer[from..to];
     }

     // ditto
     @property size_t length(){ return _length; }

     void opOpAssign(string op:"~")(ubyte[] data)
     {
         size_t end = length + data.length;
         while(end > commited)
             extend(end-commited);
         pointer[length..end] = data[];
         _length = end;
     }

     ~this()
     {
         // should not throw, struct is not copyable
         enforce(VirtualFree(pointer, 0, MEM_RELEASE));
     }
}

import std.stdio;

void timeIt(string prefix, void delegate() run)
{
     StopWatch sw;
     sw.start();
     run();
     sw.stop();
     writefln(prefix~ " %4s ms", sw.peek().msecs);
}

bool verify(Arr)(ubyte[] pattern, ref Arr arr)
{
     for(size_t i=0; i<arr.length; i+= pattern.length)
     {

         if(arr[i..i+pattern.length] != pattern[])
         {
             writeln(arr[i .. i+pattern.length], " vs ", pattern);
             return false;
         }
     }
     return true;
}

void main()
{
     size_t totalSize = 120*2^^20;
     auto chunks = [
         iota(0, 16).map!(x=>cast(ubyte)x).array,
         iota(0, 64).map!(x=>cast(ubyte)x).array,
         iota(0, 256).map!(x=>cast(ubyte)x).array,
         iota(0, 2^^10).map!(x=>cast(ubyte)x).array
     ];
     auto titles = [ " 16b", " 64b", "256b", " 16K" ];
     import core.memory;
     GC.disable(); // to prevent measuring a collection by mistake
     foreach(n, chunk; chunks)
     {
         // reserve memory anew on each iteration
         // I was able to easily go up to 1.5 G on 32bits
         // note: there are no reallocations here at all
         version(diagnose)
         if(n == 0)
         {
             writeln("Before reserving address space");
             readln();
         }
         auto virtArr = VirtualArray(totalSize);
         version(diagnose)
         if(n == 0)
         {
             writeln("Reserved address space");
             readln();
         }
         timeIt(titles[n]~" virtual", (){
             foreach(i; 0..totalSize/chunk.length)
             {
                 virtArr ~= chunk;
             }
         });
         version(diagnose)
         if(n == 0)
         {
             writeln("Commited all of virtual memory");
             readln(); //uncomment to take a pause to investigate RAM usage
         }
         // time to verify is the same for all with -O -inline
         enforce(verify(chunk, virtArr));
     }
     writeln("============");
     foreach(n, chunk; chunks)
     {
         ubyte[] arr;
         // the GC is out of the game as soon as 512 Mbytes on 32bits
         // as I hit OutOfMemory on a 2nd iteration
         // try to help poor runtime
         // without reserve it crashes and burns at about 120 M
         // with reserve it's on par with chunk >= 256
         // but it _commits_ all memory beforehand !
         version(diagnose)
         if(n == 0)
         {
             writeln("Before array.reserve()");
             readln();
         }
         arr.reserve(totalSize);
         version(diagnose)
         if(n == 0)
         {
             writeln("After array.reserve()");
             readln();
         }
         timeIt(titles[n]~" array ", (){
             foreach(i; 0..totalSize/chunk.length)
             {
                 arr ~= chunk;
             }
         });
         version(diagnose)
         if(n == 0)
         {
             writeln("After all data touched");
             readln();
         }
         // time to verify is the same for all with -O -inline
         enforce(verify(chunk, arr));
	// sadly need to not run out of memory on 32 bits
         delete arr;
     }
     writeln("============");
     foreach(n, chunk; chunks)
     {
         {
             // appender is supposed to be faster then array on appends
             // but it's actually slower starting at 256 byte chunks
             // and esp. so with 16Kb chunks
             auto app = appender!(ubyte[])();
             timeIt(titles[n]~" appender", (){
                 foreach(i; 0..totalSize/chunk.length)
                 {
                     app.put(chunk);
                 }
             });
             auto appArr = app.data();
             enforce(verify(chunk, appArr));
         }
         GC.collect();
     }
}

-- 
Dmitry Olshansky


More information about the Digitalmars-d mailing list