Þ   briarpig  » thorn  » demos  » book


demos are explained here; a menu at top column right indexes actual topic demos. Here we demo book.

problem

     When managing memory in large systems that scale, Wil must sometimes plan how much memory is used for what, and how to organize effects of granularity and fragmentation as memory is allocated and freed. One tactic is large re-usable buffers like a page but much larger in size. Wil uses the term book to name such super-page objects, which are typically 64K or 128K in size.

api clone «

     Most code comments in the left column are cloned from the page demo, which was written first. In many places the api is identical except for replacing page terms with book terms. If you memorized the page demo: the right column is new.

book heap «

yPBH heap «

     The actual heap for book allocation is shown in the (future) pile demo. It subclasses the basic yH heap interface:

class yPBH : public yH { // heap for yB pages and yB books public: yBn* hBn(yht<yBn>& firstRef, u16 id); yBn* hcBn(yht<yBn>& firstRef, u16 id); // book bytes zeroed // ... };

     Class yBn is defined further below on this page (cf »).

class yBn : public yn, private yqe { // node for yB instances public: yB* nbook() const { return n_B; } // ... };

     The hBn() and hcBn() methods each pass a yht<yBn> handle argument, receiving a first ref to a returned yBn book node whose nbook() inline method returns the yB book.

     The yB api shown next mainly defines a physical format as size in bytes, plus a bunch of convenience methods using that book of bytes. The yBn book node api further below (cf ») shows details for the node api; a book remains allocated as long as its yBn node is still in at least one handle.

book format «

     A book is basically a super-page: like an operating system's virtual memory page, but much bigger. The size of a book can vary from build to build — depending on the constant defined for size — but reasonable sizes are in the range of 64K to 128K. Larger books might only make sense in very memory intensive apps.

     Several factors influence choice of book size. Ideally a book is large enough to amortize many small operations in memory (like allocating boxed gc objects) per transition between books where discontinuity occurs. But we also want books to be small enough that allocating many of them at one time is not an onerous task for the host environment.

     A current yPBH heap allocates 128 books at a time, so if a book is 64K in size, the granularity of host environment allocation is 8MB at a time. This is small potatoes these days. You should avoid making yB so much larger than 64K that allocating 128 at a time causes pain. The goal? Big enough books, but not too big.

     An important feature of books is alignment: each book starts at an address that's a multiple of book size in bytes.

struct yB «

     The api for yB books is quite simple. The state is just a contiguous sequence of aligned bytes.

struct yB { // "book" format: yB sized & aligned alloc by yPBH « static const size_t kBsz = (64 * 1024); // 32K, 64K, 128K « typedef ptrdiff_t p_t; // ptr type convert to int for masks «

     Note a book might actually be another size: kBsz might instead be 128K or even 256K. Code comments and docs typically say 64K, as if it could not change, but the actual size of a book is whatever kBsz says. Usually it's 64K — or 0x10000 in hex, so book-aligned addresses are zero in the low four hex digits.

static const p_t klow = (kBsz - 1); // mask: in-book offsets « static const p_t kmask = ~klow; // mask book-aligned offsets «

     Constants above are used in bit masks: klow=0xffff selects bits of an address inside a book, and kmask=0xffff0000 selects the book that contains an address, in 32-bits.

u8 B_u8[ kBsz ]; // kBsz contig u8s aligned to kBsz address «

     Array B_u8[] is the array of bytes inside a book. This is the only state inside a book: the address of B_u8 is the same as the address of yB. By convention each book address is book-aligned, so not just any yB instance is valid: only those allocated at an address that's a multiple of kBsz.

     Nothing stops you from delcaring a yB anywhere you like, say at a non-aligned address. But any book allocated by yPBH will be aligned, and many yB users assert Bgood() below is true.

bool Bgood() const { « return this && (((p_t) this) & klow) == 0; }

     When Bgood() is false, you can log this using Bbad():

void Bbad(const char* where) const; // on false Bgood()

     Method p2B converts any p pointer to the yB book containing that address. This is just a matter of zeroing low bits:

static yB* p2B(const void* p) { // return book containing p « return (yB*) (void*) (kmask & ((p_t) p)); } // clear low bits

// true if aligned to yB::kBsz memory address (nil is okay): bool Baligned() const { return (((p_t) this) & klow) == 0; } «

     Method Bzero() safely clears a book to zero:

void Bzero() { « if (this->Bgood()) ::memset(this, 0, yB::kBsz); else this->Bbad("Bzero"); }

     Or Bmemset() can set all bytes to some other value:

void Bmemset(u8 val) { « if (this->Bgood()) ::memset(this, val, yB::kBsz); else this->Bbad("Bmemset"); }

     Method Bhash() returns a hash of a book's address with fewer hashmap collisions expected, using a pseudo random number generator from the rand demo:

h32 Bhash() const { return h32rand((h32) (p_t) this); } « }; // expected: sizeof(yB) == yB::kBsz, aligned to yB::kBsz

     Most yB methods are inlines, because dealing with a fix-sized format with alignment isn't complex.

yB.cpp «

     Non-inline book methods appear below. Method Bbad() logs when a book does not look good; printing a backtrace here also would help:

void yB::Bbad(const char* where) const { // on false Bgood() « if (yunlikely(!this)) yellf(__LINE__,__FILE__,"yB::Bbad(at=%s) this=nil", where); else if ( (((p_t) this) & klow) != 0 ) { yellf(__LINE__,__FILE__, "yB::Bbad(at=%s) yB=%#lx not kBsz=%#lx aligned", where, (long) this, (long) yB::kBsz); } }

     Memory management of pbooksages is done by refcounted yBn book nodes below. Under þ, books are allocated by yPBH heaps.

book nodes «

class yBn «

     Each þ yB book allocated by yPBH is refcounted using a node subclass named yBn, whose name naturally means thorn Book node. Each yB has a one-to-one correspondence with exactly one yBn node, and the source yPBH heap allocates both.

     When refs hit zero as the last handle loses the final ref to a book node, both book and node are deallocated, returning the book to a free list in the yPBH heap.

     Currently sizeof(yBn)=40 so memory management overhead for a book is forty bytes — far less than 1% — plus any additional bytes used to track the handle, which could run twenty more bytes. This overhead is quite tiny, especially when a book is later suballocated with high efficiency, avoiding further per-block overhead for objects allocated from a book.

class yBn; typedef yht<yBn> yBnh; // ref to yBn

     The yht handle template is described at length in the node demo, and here we see yBnh is a synonym for yht<yBn> whose name means thorn Book node handle.

     When the last handle with a ref to the same yBn book node loses that reference (so count of refs go to zero) the yBn node is freed, returning the yB book to a heap free pool.

class yBn : public yn, private yqe { // node refs yB instances « protected: friend class yPBH;

     Class yBn is publically a node so you can put it into a handle to increment the refcount; but it's privately a yqe queue element so it's managing heap can keep it in one list or another behind the scenes. Each heap knows the exact status of every book.

     The first two new state members n_H and n_B track the heap and actual book for the node. The heap is needed so the node can be sent back to the heap when freed (or to alloc new nodes when an old one is cloned).

yPBH* n_H; // the heap that allocated this node «

yB* n_B; // the book owned by this node «

     The n_n next node member shown below allows the node's heap to store every node in a hashmap bucket's singly linked list. (Once added to this hashmap bucket, a book node is never removed until the heap is destroyed.)

// n_n: next yBn in yBn list in some yPBH hashmap list yBn* n_n; // next book node in list «

     The next n_16 field of 16-bits is where a descriptive ID is stored, that's passed when the book is allocated.

u16 n_16; // 16-bit book ID mark «

     One byte magic signature n_ty says this node's type had better always be 'B' for book. This can be asserted when casting yn* to yBn*: it's a dynamic type tag.

u8 n_ty; enum { e_nty = 'B' } ; // "type" of this node 'B' «

     The last one byte n_fuh member indicates whether a book node is free, used, or set aside for internal heap use; the raw value is only used in unconstructed nodes. (How can nodes not yet be constructed? Space allocated, but not yet used.)

u8 n_fuh; // f=free, u=used, or h=heap (an enum val below) « enum { e_nused ='u', e_nfree ='f', e_nheap ='h', e_nraw ='r' };

     None of the state members above are public, and neither are any of the constructors below. So how can you construct a yBn book node instance? You can't. Only yPBH heaps can construct a book node — so yPBH is a factory for yBn book nodes. You can see the api to alloc a new yBn instance at the top of this page (cf «).

protected: // ONLY yPBH allocs instances of yBn yBn() : yn(), n_H(0), n_B(0), n_n(0) « , n_16(0), n_ty('B'), n_fuh(0) { } yBn(yPBH* h, yB* B, u16 id) : yn() // zero references , n_H(h), n_B(B), n_n(0), n_16(id), n_ty('B'), n_fuh('f') { } yBn(yh0& nd, yPBH* h, yB* B, u16 id) : yn(nd) , n_H(h), n_B(B), n_n(0), n_16(id), n_ty('B'), n_fuh('f') { } yBn(yht<yBn>& ref, yPBH* h, yB* B, u16 id) : yn(ref) , n_H(h), n_B(B), n_n(0), n_16(id), n_ty('B'), n_fuh('f') { }

     The getters shown below are all public. Some of them must return constant values when a book node is allocated. (But yPBH works with book nodes in other states, when they are not currently allocated.) Wil hopes the meaning of these getters is sufficiently obvious, because they won't be explained.

public: bool Bnfree() const { return e_nfree == n_fuh; } // not used «

bool Bnused() const { return e_nused == n_fuh; } «

bool Bntype() const { return e_nty == n_ty; } «

bool Bnusedtype() const { « return e_nty == n_ty && e_nused == n_fuh; }

/// \brief Bnlive() adds Bnused() to Bngood() bool Bnlive() const { « return Bnusedtype() && n_H && n_B->Bgood(); }

bool Bngood() const { « return Bntype() && n_H && n_B->Bgood(); } void Bnbadfree(const char* where) const; // Bnfree failed void Bnbad(const char* where) const; // Bngood failed

     The following size() and c_str() methods try to look at a book as a C string, but you should note the following dire warning: if you call c_str(), be advised no terminating null byte appears unless you put one inside the book yourself.

u32 size() const { return yB::kBsz; } «

const char* c_str() const { return (const char*) n_B; } «

     Method nbook() returns the node's book — this is the main method in the entire api. When given a book node, you should mainly be concerned with the book. Well, here it is:

yB* nbook() const { return n_B; } «

     The following getters and conversion operators return the book as a run, buf, or iovec. You can easily do this yourself; but by using these methods, you might avoid mistakes.

public: yv asv() const { return yv(n_B, yB::kBsz); } « operator yv() const { return yv(n_B, yB::kBsz); } «

yb asb() const { return yb(n_B, 0, yB::kBsz); } « operator yb() const { return yb(n_B, 0, yB::kBsz); } «

iovec asiovec() const { iovec v; // book as iovec « v.iov_base = (void*) n_B; v.iov_len = yB::kBsz; return v; } operator iovec() const { iovec v; // book as iovec « v.iov_base = (void*) n_B; v.iov_len = yB::kBsz; return v; }

void Bmemset(u8 val) { n_B->Bmemset(val); } «

     Bmemset() lets you call a book method on the node.

protected: // refcounted destructors are always privileged virtual ~yBn();

     As always, yn node destructors are not public: you can only destroy a node by releasing all refs, by clearing all handles that ref a given node. You can't do it by fiat.

public:// virtual yn interface // \return true if subclass looks valid (depends on subclass) virtual bool ngood() const; // \brief here a subclass can override self debug-printing //virtual void ndumpf(FILE* f) const;

public: // copy-on-write support // nclonez(): slice nclone() (if makes sense in subclass) virtual yn* nclonez(yh0& h, u32 max, iovec const& z) const; virtual yn* nclone(yh0& hout) const; virtual iovec n2iovec() const; // space as iovec

virtual const char* nclass() const { return "yBn"; } « virtual void nout(yo& o) const; virtual void ndump(yo& o) const; // self debug-printing virtual void ncite(yo& o) const; // single line debug print

     See the node demo for virtual method descriptions. Or you can follow the links to comments below on implementation.

iovec B2iovec() const { // like B2iovec(): space as iovec « iovec v; v.iov_base = (void*) n_B; v.iov_len = yB::kBsz; return v; }

     Method B2iovec() is a static, subclass specific imitation of virtual n2iovec(), when you know the type.

protected: // only _sub_strong() and _sub_weak() call: virtual void nzeroRefs(); // called when --n_refs == 0 }; // class yBn

     As always, only handles in the node demo can invoke the on-zero-refs handler. Note virtual nzeroUses() was not overriden because the default do-nothing implementation works.

yBn.cpp «

     Non-inline book node methods appear below. Most of them are very lightweight, and a couple aren't even implemented yet because Wil hasn't needed them; copy-on-write with books is still pending, but it's (of course) trivial as always.

void yBn::ncite(yo& o) const { « const char* rw = (this->nreadonly())? "r": "w"; const char* atomic = (this->natomic())? "@": "+"; char ty = (isprint(n_ty))? n_ty : '.'; char fuh = (isprint(n_fuh))? n_fuh : '.'; //u32 xcrc = (u32) ::crc32(0L, (Bytef*) n_B, yB::kBsz); o.of("<yBn me=%lx ru=%d:%d g=%02x rwi=%s%s tyl=%c%c id=%x " "B=%lx h=%lx/>", (long) this, (int) nrefs(), (int) nuses(), (int) ngen(), rw, atomic, ty, fuh, (int) n_16, (long) n_B, (long) n_B->Bhash()); }

/*virtual*/ void yBn::ndump(yo& o) const { « this->ncite(o); }

/*virtual*/ void yBn::nout(yo& o) const { « yv me(n_B, yB::kBsz); o.ov(me); }

     Method nout() writes the entire book.

/*virtual*/ iovec yBn::n2iovec() const { // space as iovec « iovec v; v.iov_base = (void*) n_B; v.iov_len = yB::kBsz ; return v; }

     Virtual n2iovec() converts to iovec the same way B2iovec() and asiovec() inlines do.

/*virtual*/ yn* yBn::nclonez(yh0& h, u32 x, iovec const& z) const { « yellf(__LINE__,__FILE__,"yBn::nclonez() not code yet"); return 0; // new(yBn::Len(z)) yBn(hout, z); }

/*virtual*/ yn* yBn::nclone(yh0& hout) const { « yellf(__LINE__,__FILE__,"yBn::nclone() not code yet"); return 0; // new(yBn::Len(*this)) yBn(hout, *this); }

     Cloning is easy using yPBH::hBn() and copy. (They'd be done already now if challege was present.)

/*virtual*/ yBn::~yBn() { n_H = 0; n_B = 0; n_16 = 0xdead; } «

     The destructor might never be called — yPBH pools nodes, never freeing pages and books until the heap is destroyed.

void yBn::Bnbadfree(const char* where) const { // Bnfree failed « yellf(__LINE__,__FILE__, "yBn::Bnbadfree(at=%s) n_fuh=%#x != e_nfree=%#x\n", where, (int) n_fuh, (int) e_nfree); }

     Checking goodness and badness is typical boilerplate in search of unexpected violations of C++ runtime guarantees.

/*virtual*/ bool yBn::ngood() const { « if (yunlikely(!this->nfine())) { this->ndead(); return false; } if (yunlikely(!this->Bnlive())) { // & used this->Bnbad("yBn::ngood"); return false; } return true; }

     On zero refs, a node frees itself using a private heap api. The mnemonic for x here in the name refers to the idea of max lifetime limit for a node, or maybe x-ing (crossing) out a node.

/*virtual*/ void yBn::nzeroRefs() { // on --n_refs == 0 « yassert(nrefs() == 0); n_H->_hxBn(this); // return to free list in yPBH }

void yBn::Bnbad(const char* where) const { // Bngood failed « if (!n_H) ylog(1, "yBn::Bnbad(at=%s) n_H=nil\n", where); else if (e_nty != n_ty) ylog(1, "yBn::Bnbad(at=%s) n_ty=%#x !- e_nty=%#x\n", where, (int) n_ty, (int) e_nty); else if (!n_B->Bgood()) ylog(1, "yBn::Bnbad(at=%s) n_B=%#lx not book aligned\n", where, (long) n_B); else if (e_nused != n_fuh) ylog(1, "yBn::Bnbad(at=%s) n_fuh=%#x !- e_nused=%#x\n", where, (int) n_fuh, (int) e_nused); else ylog(1, "yBn::Bnbad(%s) called, cause unknown\n", where); }

     Method Bnbad() must log problem descriptions.

dump formats «

     "At the bottom of the right column, why did you print the yPBH too?" Stu asked. "It made the column too long and now you have to write this dialog."

     "Just to provide more context," Wil replied. "You can see pages and books used by yBqm inside dumped yPBH output."

     "Oh you're right," Stu noticed. "Why not just remove some of the printed attributes to shorten lines?"

     "Most are at least a little informative," Wil justified. "There's more to correlate this way. Kinda verbose though."

     "Heh, I'll say," Stu sighed. "Let me ask about a couple. What's h=7d11e3ae. Page or book hash?"

     "Yeah, I think it's the crc of the page or book address," Wil agreed. "But the address itself makes a fine hash much of the time given a prime number of buckets."

     "You said 128 books were allocated at once currently," Stu recalled. "But I only see books at indexes zero to 125. That's only 126 — what happened to the other two?"

     "Two are immediately used internally without adding them to the map since they'll never been allocated publically," Wil explained. "I can map those two later if it helps."

     "What are those two books for?" Stu puzzled. "Is one for the page hashmap? At address vec=2010000?"

     "Yes, that's one of the missing books," Wil confirmed. "The other book is diced up into useful subspace right off the bat. Half of it becomes the hashmap of books at address 2008000 in the output shown. That book is the end-piece."

     "You knew I was going to ask what an end-piece book is, right?" Stu prodded, laughing. "Go on: explain."

     "Well, yPBH doesn't assume space allocated from yH has any alignment," Wil explained. "So it just asks for many books and discards one to force aligment."

     "But it's not really discarded," Stu countered.

     "That's right," Wil agreed. "All parts of the buffalo are used. A book removed to force alignment, if necessary, is then subdivided into pages, and nodes for pages and books."

     "What if you have book alignment already?" Stu asked.

     "Then I still dice one book into pages and nodes anyway," Wil elaborated, "to avoid irritating bootstrap problems."

     "Is this a preview of a future pile demo?" Stu guessed. "And why do you call yPBH a pile?"

     "Yeah, some of this material is redundant with parts of the pile demo," Wil confirmed. "But central issues can't be said enough times. I call yPBH a pile because it's a synonym for heap — and I can refer to a pile of pages and books."

     "Why not just say page/book heap every time," Stu wondered. "Is that too clunky?"

     "Because I know folks will really say 'P, B, H heap' all the time instead," Wil predicted. "Life is too short to give small things four syllable names. You can thank me now."

     "I suppose it will grow on me," Stu pondered. "Do you coin names in your day job all the time too?"

     "Yeah," Wil nodded. "And usually folks hate short names at first. The negative feedback is often quite sharp."

     "But what?" Stu prompted. "You're implying they get over it? What changes their minds?"

     "Technical discussions suddenly become crisp using terse names," Wil explained. "Suddenly long phrases become one syllable, and questions for clarification about what someone just said go way down. Talking is more clear."

     "Uh, huh," Stu was dubious. "So you say. Folks actually get enthusiastic? What works best?"

     "One syllable names that sound like nothing else work best," Wil asserted. "Especially when they replace a name that was several words."

     "Hmm," Stu considered. "Well I'd love to stay and shoot the breeze, but I have to book. See ya."

license «

     All this code is available only under the BriarPig mu-babel license described fully on the rights page. You do not have permission to reprint this page in any way. Neither feeds nor repackaging is allowed. You can link this page if you want folks to read it.

A submenu for demos appears below, letting you go to the page on a topic written as a demo (as the demos page defines it).

menu

     thorn: todo, names, fd, iovec, assert, log, run, hex, crc, buf, in, out, quote, escape, compare, file, deck, cow, arc, blob, tree, slice, rand, time, stat, hash, heap, node, primes, page, book « Þ, pile, stack, atomic, lock, mutex, thread, map, meter, list, iter, ctype

     (mu: toy, peg, imm, tag, box, symbol, token, number, bigint, class, method, reader, writer, eval, env, vm, gc, world, pcode, compiler, asm, lathe, lisp, smalltalk, design, weight, jar, card, harp, debug, profile)

     Some demos are stubs: todo is a demo guide. See toy for mu updates on language pages; names introduces naming schemes.

similarity «

     Parts of this book demo strongly resemble the page demo because a yB book is similar to a yP page, just bigger. And a yBn book node bears the same relation to yB that a yPn page node bears to yP. So code for book nodes and maps will closely parallel code for page nodes and maps.

     However, pages don't have the same sort of role in garbage collection as books. The next section explains.

garbage collection «

     The design of 64K (or 128K) books in þ is specifically geared to support a form of garbage collection Wil designed around 2000, give or take a year, which manages large areas of space with higher granularity than classical garbage collection systems. Instead of finding a link to old public descriptions from several years ago, consider the following short summary and recapitulation of basic ideas.

     A classical form of copying collection often describes available memory as consisting of two large, equal-sized partitions — let's call them hemispheres or hemis for short — where at any given time one hemi is in use and the other hemi is held in reserve. When gc (garbage collection) is required, one hemi is copied to the other; then they switch.

     The bad part of this classical design is obvious: half of memory is wasted. The hemi held in reserve isn't useful for anything except ensuring the used hemi can be copied when space is exhausted.

     Even worse, there's a problem in deciding how big memory should be. If you have two large hemis, how do you gracefully make these memory partitions bigger? Growing the basic memory footprint in a hemi model is very awkward at best. Wil wondered why folks didn't solve this problem using an obvious simplification.

     Instead of using a couple hemis, or some small number of them, why not define each partition as a collection of smaller pieces? (Call each piece a book since books on this page are designed to fill exactly this gc role Wil dreamed up.) Then several issues get easier to address. Want to make the size of memory bigger? Just allocate more books. Ready to collect garbage? How many books do you need to keep in reserve for gc? Only as many as you might collect at one time. If you partition the address space into disjoint book set partitions, each partition can act as a lightweight process garbage collected independently.

     Wil's plan to use books for gc aimed to reduce the size of dead space held in reserve for gc, because only enough reserve to copy the largest lightweight process is needed. If you craft your app partitions with care, this can be far less than half all your available memory.

     Better yet, the fewer books in a lightweight process, the faster it can be garbage collected. A single lightweight "process" consisting of a few megs of books can be collected in a few milliseconds — the time is mainly how long it takes to touch that much memory.

     The hashmap of books shown in the next section aims to represent the address space of a lightweight "process" using garbage collection in a set of books. One of the things it must do very quickly is answer the question, "Is this pointer inside this 'process' address space?" In other words, the map must very quickly say whether a pointer is contained by a book in the map. Even the cost of hashing a book address might cost too much. So the map must use a prime number of buckets.

     Wil plans to implement a classic Cheney style stop-and-copy gc in the first toy language implementation. And part of this algorithm requires telling whether an object must be copied or not. A classical description of Cheney uses an address range check to see whether an object lives inside the source hemi being copied. But when Wil uses books to compose gc address spaces, the range check must take the form of a hashmap key lookup: is this pointer's book inside source books being copied?

     Among other things, this design allows layered gc address spaces. One address space of books can hold long-lived objects — perhaps immutable objects never collected. A mutable address space can refer to objects in the immutable space, and this will cost nothing in gc to discriminate because it will already be necessary to check every object for membership in a source set of books being copied. For sandboxing applications, it will be easy to copy-on-write starter address spaces cloned into a smaller footprint sandbox, which can simply be thrown away when a sandboxed request is done.

     In other words, the use of books in garbage collection is critical for the kinds of optimizations Wil plans to use, and they seem exciting, if a little off the beaten path.

book queue map «

     Part of the class below will strongly resemble maps in the page demo's right column. Some of the interfaces used are defined in the page demo. For example, the yPq page queue class is used to hold references to page nodes, so the pages can be used to build data structures while yPq handles memory management (cf «).

     However, unlike the page demo maps, the map below allocates the book members inside: it's a factory for book instances which automatically owns and tracks the set of books in a hashmap. The idea is to provide a growable dynamic address space with book granularity. (To garbage collect, Wil can make a new yBqm map to copy old books, then destroy the previous instance to release old books processed.)

struct yBqme : public yqnhe { // hashmap bucket list elem « yBqme* me_e; // the next element in a hashmap bucket « yB* me_B; // same ptr as ((yBn*) e_nh.hn())->nbook() «

bool egood() const { // book is same as one in my node: « return me_B == ((yBn*) e_nh.hn())->nbook(); }

yBqme() : yqnhe(), me_e(0), me_B(0) { } « yBqme(yBn* n, yB* B) : yqnhe(n), me_e(0), me_B(B) { } ~yBqme() { } // releases node if not already released }; // struct yBqme (primarily queue and map elems in yBqm)

     Class yBqme is the book queue map element used to add hashmap members to yBqm below. The key (book pointer) is stored in me_B, and me_e is just a link to the next elem in the same hashmap bucket, where each bucket is a singly linked list.

     Note yBqme is a subclass of yqnhe which has a handle that refs a node. Each yBqme elem instance holds a ref to a book's node; this is how the queue in yBqm manages owership of every book inside.

class yBqm «

     The name of class yBqm means thorn Book queue (and) map, but it's really just a map of books. The q for queue means yBqm is both a queue of books and a map of books. The queue is list of handles that ref book nodes; the map indexes these books by address.

     Each element in a map bucket is a yBqme element, defined immediately above. (In the page demo, comparable map elements were nested classes.)

class yBqm { // queue and hashmap of yPBH-allocated books « private: // hashmap uses yBqme elems

// we could use 1/2 page for buckets; but let's use it all: static const u32 kprime1k = e1i_1021_1k; // prime < 1K « static const u32 kslots = e1i_1021_1k; // prime filling 4K «

     The number of buckets is kslots: the largest prime number less than 1024, because one 4096 byte page can hold at most 1024 yBqme* bucket pointers, assuming each pointer is 32-bits. Here you see assumptions about both page size and pointer size.

yPBH* m_H; // source of all books and pages « yPq m_Pq; // pages allocated (hashmap & yBqme elems) « // when m_vH is exhausted, we alloc a page of yBqme elems: yv m_vH; // space remaining in page for yBqme elems « yl32 m_le; // list of e=yBqme, each new'ed from m_vH « // m_epp is 1st page alloc'd in m_Pq during _minit(): yBqme** m_epp; // hashmap array: bucket[kslots], one page «

     The last state member m_epp above is the array of buckets, whose name means element pointer pointer since the hashmap is an array of element pointers; m_epp points to the start of a yP page — the first one allocated from heap m_H by the m_Pq list of pages.

     Iovec m_vH is the remains of the last page allocated for the purpose of providing space for yBqme element allocations. The name of m_vH means (u8) vector heap, because this yv run is treated as a fix-sized heap for suballocation. When exhausted, m_vH is replenished simply by allocating another page from m_Pq.

     When first allocated, new yBqme elems allocated from m_vH go straight into the m_epp hashmap after construction when new members are added. Each new elem is also added to list of elements m_le, which is a list of book members held in handles.

     The current number of map members is the length of m_le, which is a list of all books add to the map.

bool _minit(); // true if good 1st init when m_epp=nil yBqme* _mnew(); // alloc new elem from m_vH or new page yo& _mdumpB(yo& o) const; // dump book hashmap

     Follow links above to code and explanatory comments.

public: // default id='Bq'=0x4271: yB* mnewB(u16 id=0x4271); // alloc book owned by yBqm /// \param p can ANY address; we'll look for it's book /// \return fast answer: Is p in a book in THIS map? yB* mfindB(const void* p) const; // book for p, else nil

     Method mnewB() allocates a new book from heap m_H, adding its node to the queue and the book address to the map.

     Method mfindB() returns the book containing p if and only if that book is a member of the map.

explicit yBqm(yPBH* heap); // all allocations in heap void mclear(); // like ~yBqm(), but yBqm stays usable

     The destructor and mclear() both release all books and empty the map, which remains usable after mclear() is called, which returns the map to the same empty state existing just after construction.

n32 msize() const { return m_le.lsize(); } // book length « ~yBqm(); // mclear() releases all books and pages to heap

     The number of books now in the map is msize().

struct Mq { yBqm const& q_m; Mq(yBqm const& m): q_m(m) {} }; « Mq quote() const { return Mq(*this); } // request dump « void mprint() const; // dump to stdout via yout void mdump(yo& o) const; void mcite(yo& o) const; }; // class yBqm

     Api details for printing boilerplate above and below are shown in out and quote demos; sample use appears in other demos.

inline yo& operator<<(yo& o, yBqm const& x) { « x.mdump(o); return o; } inline yo& operator<<(yo& o, yBqm::Mq const& x) { « x.q_m.mdump(o); return o; } inline yo& operator<<(yo& o, yct<yBqm> const& x) { « x.c_t.mcite(o); return o; }

yBqm.cpp «

     Non-inline methods for yBqm appear below.

yBqm::yBqm(yPBH* heap) « : m_H(heap), m_Pq(heap), m_vH(0,0), m_le(), m_epp(0) { // wait until 1st call to mnewB() before calling _minit() }

     The constructor needs a heap from which to allocate books and pages. But nothing is allocated until a book is requested via mnewB().

yBqm::~yBqm() { this->mclear(); m_H = 0; } // nil: stop repeat «

     The destructor releases all resources.

void yBqm::mclear() { « if (yunlikely(!m_H)) { yH::hnil("yBqm::m_H"); return; } yBqme* e = 0; while ((e = (yBqme*) m_le.lpop()) != 0) e->qnheclear(); // release book ref'd by this node handle // do NOT free yBqme instances block allocated from pages m_vH.vclear(); m_epp = 0; // buckets re-allocated again later by _minit() m_Pq.qclear(); // free pages only AFTER any space inside }

     Clearing the map destroys two separate lists. The list of books in m_le is popped, clearing each elem inside, which releases all the books. Note instances of yBqme are not deallocated because they were allocated from pages stored in the other list, m_Pq, holding all pages allocated.

bool yBqm::_minit() { « // true if succeeds first init when m_epp starts at nil if (!m_epp) { // still need to alloc buckets? yP* P = m_Pq.qnewP(/*hb*/0x6862); // hashmap buckets if (yunlikely(!P)) { yH::hnil("m_epp"); return false; } P->Pzero(); // zero all page bytes for nil bucket slots m_epp = (yBqme**)(void*) P; // page as map bucket array } return true; // buckets ready to use }

     If not already previously initialized, _minit() allocs the first zero page for a m_epp hashmap consisting of all empty buckets.

yBqme* yBqm::_mnew() { « // alloc a new elem from m_vH if possible, else new page if (yunlikely(!m_H || (!m_epp && !this->_minit()))) return 0; // nothing can be done void* mem = m_vH.vtake(sizeof(yBqme)); // from last page if (!mem) { // must alloc a new page broken into elems? yP* P = m_Pq.qnewP(0x4271); if (yunlikely(!P)) { yH::hnil("_mnew"); return 0; } m_vH.vinit(P, yP::kPsz); mem = m_vH.vtake(sizeof(yBqme)); } if (mem) // raw memory for construction? return new(mem) yBqme; // using placement operator new return 0; }

     When a new member is added, _mnew() allocs a yBqme element to hold the new book node, trying to alloc sizeof(yBqme) bytes from the last page put in m_vH using yv::vtake(), which is essentially just allocation by pointer bumping. Failing that, a new page is allocated from m_Pq, which holds the page node in a handle for later release.

     Finally, provided an elem was allocated, _mnew() calls placement operator new() which returns the pointer passed and calls the constructor on this space. This use of placement operator new() is just the C++ way to call a constructor by name on heap space.

yB* yBqm::mnewB(u16 bid) { « // alloc book owned by yBqm, but usable for whatever yBqme* e = this->_mnew(); if (yunlikely(!e)) return 0; // bad state or out of memory yBnh first; yBn* n = m_H->hBn(first, /*eg: id='Bq'*/ bid); if (yunlikely(!n)) { // heap did not alloc book? ylog(0, "mnewB() yPn=nil\n"); return 0; } e->e_nh = n; // put node ref into elem m_le.lpush_back(e); // append elem yB* B = n->nbook(); if (yunlikely(!n->Bngood())) n->Bnbad("yBqm::mnewB"); // "hash": just book address: must match lookup in mfindB() yBqme** pp = m_epp + (((yB::p_t) B) % yBqm::kslots); // bucket e->me_e = *pp; *pp = e; // push new entry on bucket list e->me_B = B; return B; // return book inside the node }

     Allocating a new book with mnewB() is straight forward. First _mnew() allocates a new yBqme with a handle that can ref a new book node once allocated. If this succeeds, a book is allocated from the heap.

     If both those succeed, the new elem is appended to list of elements m_le and pushed on the top of the map bucket to which the book address hashes. The hash of a book here is the book's own address. For an explanation why this might be expected to work very well, see the hash demo — particularly the final identity hash section, which suggests very few collisions for naked book addresses are expected.

yB* yBqm::mfindB(const void* p) const { « // return book in yBqm for p, else nil // omit check; mfindB() might be called in tight gc loop: // if (!m_H || !m_epp) { return 0; } // bad/gone/empty: yB* B = yB::p2B(p); // clear low bits: book containing p // ((yB::p_t) B), not B->Bhash(), for fast gc tight loops: yBqme** pp = // hashmap bucket for B m_epp + (((yB::p_t) B) % yBqm::kslots); for (yBqme* e = *pp; e; e = e->me_e) { // more bucket? if (B == e->me_B) // found B in list? return B; // success } return 0; // could not find in bucket where B must be }

     Method mfindB() looks for the book in the map as cheaply as possible, using the same hash employed by mnewB(). Expected elapsed time in mfindB() must be very small because it will be called many times during garbage collection. The code in mfindB() will be used in the center of a tight loop, so cost must be minimized.

void yBqm::mprint() const { « yout << yendl; this->mdump(yout); yout << yendl << ynow; }

void yBqm::mdump(yo& o) const { « n32 n = m_le.lsize(); if (yunlikely(!n)) { this->mcite(o); return; } // empty? o.oft("<yBqm me=%lx H=%lx epp=%lx Blen=%u Plen=%u vn=%u>", (long) this, (long) m_H, (long) m_epp, (int) m_le.lsize(), (int) m_Pq.qsize(), (int) m_vH.v_n); o << yendl << m_Pq.quote(); // print queue of pages /*unsigned i = 0; char arc[32]; for (yqp p = m_le.lbegin(); p != m_le.lend(); ++p) { ::sprintf(arc, "%u", ++i); const yBqme* e = (const yBqme*) (const yqe*) p; if (e) o << yendl << e->e_nh.cite(arc); else o << "<yBqme e=nil/>"; // how can this happen? }*/ o << yendl; this->_mdumpB(o); o.ounend("yBqm"); }

void yBqm::mcite(yo& o) const { « o.of("<yBqm me=%lx H=%lx Blen=%u Plen=%u vn=%u/>", (long) this, (long) m_H, (int) m_le.lsize(), (int) m_Pq.qsize(), (int) m_vH.v_n); }

yo& yBqm::_mdumpB(yo& o) const { int sz = 0; « o.oft("<map vec=%lx slots=%d sz=%d>" " <!-- sizeof(yBqme)=%d -->", (long) m_epp, (int) kslots, (int) m_le.lsize(), sizeof(yBqme)); for (unsigned i = 0; m_epp && i < kslots; ++i) { yBqme* p = m_epp[i]; if (p) { o.on(); // anything in this bucket? if (p->me_e) { // more than one in list? // print N nodes inside a list tag? o.oft("<lB i=%d>", (int) i); // index in list tag n32 len = 0; for ( ; p; p = p->me_e) { if (++len > 1) o << yendl; ++sz; o << p->e_nh->yn::quote(); } o.ou(); // append trailing count: o.of("</lB> <!-- %d -->", (int) sz); } else { // otherwise print node by itself o << p->e_nh->yn::quote(); o.of(" <!-- %d i=%d -->", ++sz, (int) i); } } } o.ounend("map"); return o; }

sample «

     Output from this sample code appears in the next section. This merely creates stack instances of yPBH and yBqm, allocates two pages from the latter, and prints each before and after.

int main(int argc, char * const argv[]) { « yH& vanillaHeap = yH::g_1H; // malloc() and free() yPBH heap(&vanillaHeap); // allocs pages and books

yout << yendl << "#empty heap:" << yendl << heap.quote(); yout << yendl << ynow; // (see output »)

{ // scope for lifetime of Bspace yBqm Bspace(&heap);

yout << "#empty Bspace:" << yendl << Bspace.quote(); yout << yendl << ynow; // (see output »)

yB* firstB = Bspace.mnewB(); yB* secondB = Bspace.mnewB();

yout << yendl; yout.ofn("# firstB=%#lx secondB=%#lx", (long) (yB::p_t) firstB, (long) (yB::p_t) secondB); yout << yendl << Bspace.quote(); yout << yendl << ynow; // (see output »)

yout << yendl << "#used heap:" << yendl << heap.quote(); yout << yendl << ynow; // (see output ») } return 0; }

     Each output link in code above takes you to a corresponding stdout section below.

stdout «

     The following edited output appears on stdout — long lines were broken to fit this column. The extra line break is shown as one ¬ character. Note this understandably looks much more cluttered than a single-line original.

     One hundred lines were removed from the final heap dump output as annotated in red. (You won't miss them.)

#empty heap: « <yPBH me=bffff9a4 H=g_1H qve=0 fB=0 uB=0 hB=0 rP=0 fP=0 ¬ uP=0 hP=0 > <books vec=0 slots=8191 size=0> <!-- sizeof(yBn)=40 --> </books> <pages vec=0 slots=16381 size=0> <!-- sizeof(yPn)=40 --> </pages> <kilos vec=0 slots=16381 size=0> <!-- sizeof(yKn)=40 --> </pages> </yPBH>

#empty Bspace: « <yBqm me=bffffa58 H=bffff9a4 Blen=0 Plen=0 vn=0/>

# firstB=0x2030000 secondB=0x2040000 « <yBqm me=bffffa58 H=bffff9a4 epp=2802000 Blen=2 Plen=2 vn=4048> <yPq me=bffffa5c H=bffff9a4 len=2> <yh0 a=1 k=S n=28013a8 g=6f><yPn me=28013a8 ru=1:1 g=6f¬ rwi=w+ tyl=Pu id=6862 P=2802000 h=7d11e3ae/></yh0> <yh0 a=2 k=S n=28013d0 g=db><yPn me=28013d0 ru=1:1 g=db¬ rwi=w+ tyl=Pu id=4271 P=2803000 h=8dad3af/></yh0> </yPq> <map vec=2802000 slots=1021 sz=2> <!-- sizeof(yBqme)=24 --> <yBn me=2800038 ru=1:1 g=0c rwi=w+ tyl=Bu id=4271 B=2040000¬ h=103c02f8/> <!-- 1 i=35 --> <yBn me=2800010 ru=1:1 g=42 rwi=w+ tyl=Bu id=4271 B=2030000¬ h=53ad02f6/> <!-- 2 i=864 --> </map> </yBqm>

#used heap: « <yPBH me=bffff9a4 H=g_1H qve=1 fB=123 uB=2 hB=2 rP=97 fP=3¬ uP=2 hP=2 > <books vec=2008000 slots=8191 size=125> <!-- sizeof(yBn)=40 --> <yBn me=28007e0 ru=0:0 g=ff rwi=w+ tyl=Bf id=0 B=2350000¬ h=279b0340/> <!-- 1 i=34 --> <yBn me=2800010 ru=1:1 g=42 rwi=w+ tyl=Bu id=4271 B=2030000¬ h=53ad02f6/> <!-- 2 i=115 --> <yBn me=2800e98 ru=0:0 g=2b rwi=w+ tyl=Bf id=0 B=2600000¬ h=53a0037f/> <!-- 3 i=148 --> <yBn me=28006c8 ru=0:0 g=a5 rwi=w+ tyl=Bf id=0 B=22e0000¬ h=7fb20335/> <!-- 4 i=229 --> <yBn me=2800d80 ru=0:0 g=1c rwi=w+ tyl=Bf id=0 B=2590000¬ h=2bb70375/> <!-- 5 i=312 --> <yBn me=28005b0 ru=0:0 g=e9 rwi=w+ tyl=Bf id=0 B=2270000¬ h=57c9032b/> <!-- 6 i=393 --> <yBn me=2800c68 ru=0:0 g=7e rwi=w+ tyl=Bf id=0 B=2520000¬ h=3ce036b/> <!-- 7 i=476 --> <yBn me=2800498 ru=0:0 g=73 rwi=w+ tyl=Bf id=0 B=2200000¬ h=2fe00321/> <!-- 8 i=557 --> <yBn me=2801320 ru=0:0 g=b2 rwi=w+ tyl=Bf id=0 B=27d0000¬ h=2fd303aa/> <!-- 9 i=590 --> <yBn me=2800b50 ru=0:0 g=c4 rwi=w+ tyl=Bf id=0 B=24b0000¬ h=5be50360/> <!-- 10 i=671 --> <yBn me=2800380 ru=0:0 g=b0 rwi=w+ tyl=Bf id=0 B=2190000¬ h=7f70317/> <!-- 11 i=721 --> <yBn me=2801208 ru=0:0 g=ee rwi=w+ tyl=Bf id=0 B=2760000¬ h=7ea03a0/> <!-- 12 i=754 --> <yBn me=2800a38 ru=0:0 g=80 rwi=w+ tyl=Bf id=0 B=2440000¬ h=33fc0356/> <!-- 13 i=835 --> <yBn me=2800268 ru=0:0 g=2b rwi=w+ tyl=Bf id=0 B=2120000¬ h=600e030c/> <!-- 14 i=916 --> <yBn me=28010f0 ru=0:0 g=72 rwi=w+ tyl=Bf id=0 B=26f0000¬ h=60010395/> <!-- 15 i=949 --> <yBn me=2800920 ru=0:0 g=e0 rwi=w+ tyl=Bf id=0 B=23d0000¬ h=c13034c/> <!-- 16 i=999 --> <yBn me=2800150 ru=0:0 g=47 rwi=w+ tyl=Bf id=0 B=20b0000¬ h=38250302/> <!-- 17 i=1080 --> <yBn me=2800fd8 ru=0:0 g=5a rwi=w+ tyl=Bf id=0 B=2680000¬ h=3818038b/> <!-- 18 i=1113 --> <yBn me=2800808 ru=0:0 g=b1 rwi=w+ tyl=Bf id=0 B=2360000¬ h=642a0341/> <!-- 19 i=1194 --> <yBn me=2800038 ru=1:1 g=0c rwi=w+ tyl=Bu id=4271 B=2040000¬ h=103c02f8/> <!-- 20 i=1244 --> <yBn me=2800ec0 ru=0:0 g=6a rwi=w+ tyl=Bf id=0 B=2610000¬ h=102f0381/> <!-- 21 i=1277 --> <yBn me=28006f0 ru=0:0 g=3e rwi=w+ tyl=Bf id=0 B=22f0000¬ h=3c410337/> <!-- 22 i=1358 --> (one hundred lines of book nodes elided here for brevity) <yBn me=28008f8 ru=0:0 g=00 rwi=w+ tyl=Bf id=0 B=23c0000¬ h=4f84034a/> <!-- 123 i=8061 --> <yBn me=2800128 ru=0:0 g=bb rwi=w+ tyl=Bf id=0 B=20a0000¬ h=7b960300/> <!-- 124 i=8142 --> <yBn me=2800fb0 ru=0:0 g=f3 rwi=w+ tyl=Bf id=0 B=2670000¬ h=7b890389/> <!-- 125 i=8175 --> </books> <pages vec=2010000 slots=16381 size=5> <!-- sizeof(yPn)=40 --> <yPn me=28013a8 ru=1:1 g=6f rwi=w+ tyl=Pu id=6862 P=2802000¬ h=7d11e3ae/> <!-- 1 i=203 --> <yPn me=2801448 ru=0:0 g=90 rwi=w+ tyl=Pf id=0 P=2806000¬ h=2c35a3af/> <!-- 2 i=13897 --> <yPn me=2801420 ru=0:0 g=4d rwi=w+ tyl=Pf id=0 P=2805000¬ h=206cb3af/> <!-- 3 i=14551 --> <yPn me=28013f8 ru=0:0 g=32 rwi=w+ tyl=Pf id=0 P=2804000¬ h=14a3c3af/> <!-- 4 i=15205 --> <yPn me=28013d0 ru=1:1 g=db rwi=w+ tyl=Pu id=4271 P=2803000¬ h=8dad3af/> <!-- 5 i=15859 --> </pages> <kilos vec=2020000 slots=16381 size=0> <!-- sizeof(yKn)=40 --> </pages> </yPBH>