Þ   briarpig  » thorn  » demos  » page


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

problem «

     Whether or not virtual memory is a factor in some app or system, Wil often uses page buffers as one granularity of heap allocation, for several reasons:

  • aligned with virtual memory page costs
  • uniform memory re-use cuts fragmentation
  • cheap page containment tests for pointers
  • better scaling under VM page pressure

     Under þ, pages are allocated by the yPBH heap api shown in the pile demo. The page allocation api strongly resembles the api for yB book allocation. The two yPBH methods involved — hPn() and hcPn() — both allocate nodes and are shown below.

page heap «

yPBH heap «

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

class yPBH : public yH { // heap for yP pages and yB books public: yPn* hPn(yht<yPn>& firstRef, u16 id); yPn* hcPn(yht<yPn>& firstRef, u16 id); // page bytes zeroed // ... };

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

class yPn : public yn, private yqe { // node for yP instances public: yP* npage() const { return n_P; } // ... };

     The hPn() and hcPn() methods each pass a yht<yPn> handle argument, receiving a first ref to a returned yPn page node whose npage() inline method returns the yP page.

     The yP api shown next mainly defines a physical format as size in bytes, plus a bunch of convenience methods using that page of bytes. The yPn page node api further below (cf ») shows details for the node api; the page remains allocated as long as its yPn node is still in at least one handle. A yPq page queue api shown later on this page (cf ») is a good place to hold page nodes. Hashmaps shown in the right column use yPq to own pages used in maps.

page format «

     In most operating systems with virtual memory, a page is the unit size of paging (yes: sounds tautological) content between memory and secondary storage. Typically pages are 4K in size, but some systems use 8K, etc. In fact, you can't easily presume anything about page size in totally portable code. But material below pretends pages are always 4K in size.

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

struct yP «

     The api for yP pages is quite simple. The state is just a contiguous sequence of aligned bytes.

struct yP { // "page" format: yP-sized & aligned from yPBH « static const size_t kPsz = (4 * 1024); // either 4K or 8K « typedef ptrdiff_t p_t; // converts ptr to int for masks «

     Note a page might actually be another size: kPsz might instead be 8192 on systems using 8K pages. Code comments and docs typically say 4K, as if it could not change, but the actual size of a page is whatever kPsz says. Usually it's 4K — or 0x1000 in hex, so page-aligned addresses are zero in the low three hex digits.

static const p_t klow = (kPsz - 1); // mask: in-page offsets « static const p_t kmask = ~klow; // to page-align offsets «

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

u8 P_u8[ kPsz ]; // contiguous + aligned to kPsz multiple «

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

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

bool Pgood() const { // non-nil and page aligned: « return this && (((p_t) this) & klow) == 0; } // true if aligned to yP::kPsz memory address (nil is okay): bool Paligned() const { return (((p_t) this) & klow) == 0; } «

     When Pgood() is false, you can log this using Pbad():

void Pbad(const char* where) const; // on false Pgood()

     Method p2P converts any p pointer to the yP page containing that address. This is just a matter of zeroing low bits:

static yP* p2P(const void* p) { // return page containing p « return (yP*) (void*) (kmask & (p_t) p); } // clear low bits

     Method Pzero() safely clears a page to zero:

void Pzero() { // clear all bytes to zero « if (this->Pgood()) ::memset(this, 0, yP::kPsz); else this->Pbad("Pzero"); }

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

void Pmemset(u8 val) { // set every page byte to val « if (this->Pgood()) ::memset(this, val, yP::kPsz); else this->Pbad("Pmemset"); }

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

h32 Phash() const { return h32rand((h32) (p_t) this); } « void Pcopy(const yP* src); // make page a copy of src }; // yP, expected: sizeof(yP) == kPsz & aligned to yP::kPsz

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

yP.cpp «

     Non-inline page methods appear below. Method Pcopy() copies a source page to this page:

void yP::Pcopy(const yP* src) { // make page copy of src « if (this->Pgood()) { if (src->Pgood()) ::memcpy(this, src, yP::kPsz); else { src->Pbad("Pcopy"); this->Pzero(); } } else this->Pbad("Pcopy"); }

     Method Pbad() logs when a page does not look good; printing a backtrace here also would help:

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

     Memory management of pages is done by refcounted yPn page nodes below. Under þ, pages are allocated by yPBH heaps.

page nodes «

class yPn «

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

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

     Currently sizeof(yPn)=40 so memory management overhead for a page is forty bytes — near one percent — plus any additional bytes used to track the handle, which could run twenty more bytes. This overhead is quite moderate, especially when a page is later suballocated with high efficiency, avoiding further per-block overhead for objects allocated from a page.

class yPn; typedef yht<yPn> yPnh; // ref to yPn «

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

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

class yPn : public yn, private yqe { // node for yP instances protected: friend class yPBH;

     Class yPn 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 page.

     The first two new state members n_H and n_P track the heap and actual page 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 « yP* n_P; // the page 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 page node is never removed until the heap is destroyed.)

// n_n: next yPn in yPn list in some yPBH hashmap list yPn* n_n; // next page node in list «

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

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

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

u8 n_ty; enum { e_nty = 'P' } ; // "type" of node: 'P' «

     The last one byte n_fuh member indicates whether a page 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 (enum val below) « enum { e_nused ='u', e_nfree ='f', e_nheap ='h', e_nraw ='r' };

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

protected: // ONLY class yPBH allocates instances of yPn yPn() : yn(), n_H(0), n_P(0), n_n(0) « , n_16(0), n_ty('P'), n_fuh(0) { } yPn(yPBH* h, yP* P, u16 id) : yn() // zero references , n_H(h), n_P(P), n_n(0), n_16(id), n_ty('P'), n_fuh('f') { } yPn(yh0& nd, yPBH* h, yP* P, u16 id) : yn(nd) , n_H(h), n_P(P), n_n(0), n_16(id), n_ty('P'), n_fuh('f') { } yPn(yht<yPn>& ref, yPBH* h, yP* P, u16 id) : yn(ref) , n_H(h), n_P(P), n_n(0), n_16(id), n_ty('P'), n_fuh('f') { }

     The getters shown below are all public. Some of them must return constant values when a page node is allocated. (But yPBH works with page 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 Pnfree() const { return e_nfree == n_fuh; } // unused «

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

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

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

/// \brief Pnlive() adds Pnused() to Pngood() bool Pnlive() const { « return Pnusedtype() && n_H && n_P->Pgood(); }

bool Pngood() const { « return Pntype() && n_H && n_P->Pgood(); }

void Pnbadfree(const char* where) const; // Pnfree failed void Pnbad(const char* where) const; // Pngood failed

     The following size() and c_str() methods try to look at a page 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 page yourself.

u32 size() const { return yP::kPsz; } « const char* c_str() const { return (const char*) n_P; } «

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

yP* npage() const { return n_P; } «

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

yv asv() const { return yv(n_P, yP::kPsz); } « operator yv() const { return yv(n_P, yP::kPsz); } «

yb asb() const { return yb(n_P, 0, yP::kPsz); } « operator yb() const { return yb(n_P, 0, yP::kPsz); } «

iovec asiovec() const { iovec v; // the page as an iovec « v.iov_base = (void*) n_P; v.iov_len = yP::kPsz; return v; } operator iovec() const { iovec v; // the page as an iovec « v.iov_base = (void*) n_P; v.iov_len = yP::kPsz; return v; }

void Pmemset(u8 val) { n_P->Pmemset(val); } «

     Pmemset() lets you call a page method on the node.

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

     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 // true if subclass looks valid (depending on subclass) virtual bool ngood() const;

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

virtual const char* nclass() const { return "yPn"; } « 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 P2iovec() const { // space as an iovec « iovec v; v.iov_base = (void*) n_P; v.iov_len = yP::kPsz; return v; }

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

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

     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.

yPn.cpp «

     Non-inline page 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 pages is still pending, but it's — you guessed it — trivial as always).

void yPn::ncite(yo& o) const { // single line debug print « 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) -1; if (this->Pntype() && n_P->Pgood()) // safe to use page? xcrc = (u32) ::crc32((uLong) 0L, (Bytef*) n_P, yP::kPsz); o.of("<yPn me=%lx ru=%d:%d g=%02x rwi=%s%s tyl=%c%c id=%x " "P=%lx h=%lx crc=%lx/>", (long) this, (int) nrefs(), (int) nuses(), (int) ngen(), rw, atomic, ty, fuh, (int) n_16, (long) n_P, (long) n_P->Phash(), (long) xcrc); }

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

/*virtual*/ void yPn::nout(yo& o) const { « yv me(n_P, yP::kPsz); o.ov(me); }

     Method nout() writes the entire page.

/*virtual*/ iovec yPn::n2iovec() const { // space as iovec « iovec v; v.iov_base = (void*) n_P; v.iov_len = yP::kPsz; return v; }

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

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

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

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

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

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

void yPn::Pnbadfree(const char* where) const { // Pnfree fail « yellf(__LINE__,__FILE__, "yPn::Pnbadfree(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 yPn::ngood() const { « if (yunlikely(!this->nfine())) { ndead(); return false; } if (yunlikely(!this->Pnlive())) { this->Pnbad("yPn::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 yPn::nzeroRefs() { // on --n_refs == 0 « yassert(nrefs() == 0); n_H->_hxPn(this); // return to free list in yPBH }

void yPn::Pnbad(const char* where) const { // Pngood() failed « if (!n_H) ylog(1, "yPn::Pnbad(at=%s) n_H=nil\n", where); else if (e_nty != n_ty) ylog(1, "yPn::Pnbad(at=%s) n_ty=%#x !- e_nty=%#x\n", where, (int) n_ty, (int) e_nty); else if (!n_P->Pgood()) ylog(1, "yPn::Pnbad(at=%s) n_P=%#lx not page aligned\n", where, (long) n_P); 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, "yPn::Pnbad(%s) called, cause unknown\n", where); }

     Method Pnbad() must log problem descriptions.

     The next section shows how yPn page nodes can be owned by lists based on primitive yq queues from the list demo.

page lists «

     The name of class yPq means thorn Page queue. It uses primitive queues based on yq in the list demo to manage a set of yPn page nodes. So far its main purpose in practical use has been simple page node ownership for allocated pages.

     In effect, you just call qnewP() to say "give me a new yP page" which gets allocated from a yPBH heap. Then you use the yP page however you please, safe in knowledge this new page remains allocated by means of its node held by a yPnh handle within yPq. When a yPq list is destroyed, all pages are released at once. As a result, yPq acts a bit like a RAII guard for as many pages as you want to allocate for some purpose.

     Page-based hashmaps shown column right use yPq for exactly this purpose, allocating pages from yPq, then using them without paying any attention to memory management because yPq handles that. This coding style gets leverage through composition, using other objects for narrow effects.

     This version of yPq is slightly too primitive to be most efficient in touching memory, because it allocs list elements one at a time, instead of many at once at once in a bigger block. Using a page for this purpose seemed too high of overhead for short page lists. A simple variation of yPq was almost added to this demo to show better memory style, but it was omitted for brevity. (But a new 1K kilo object added to yPBH's api would work for this purpose.)

class yPq «

     This queue of pages is mainly a shallow api to manage the two first state member variables, q_H and q_le shown below:

class yPq { // yqnhe based queue of yPBH-allocated pages « private: // owns pages used by objects to provide bulk space yPBH* q_H; // source of all pages and yqnhe's allocated « yl32 q_le; // yqnhe list, separately new'ed from q_H «

     Queue heap q_H is the source of all pages allocated, and q_le is the list of elements holding a yPn page node for each page allocated. Elements in q_le are type yqnhe shown in the node demo (cf «). Since list elems are not exposed publically, generic node handle element yqnhe was used instead of a handle elem specfic to yPn page nodes.

public: // default id='Pq'=0x5071: // yPq::qnewP() is main purpose of yPq; does all bookkeeping // return new yP-aligned page, freed later by yPq:: qclear() yP* qnewP(u16 id=0x5071); // alloc new page owned by yPq void qclear(); // same as ~yPq() but yPq remains usable

     Method qnewP() adds a new page to the list and returns it, while qclear() empties the list completely, freeing every page previously allocated. Obviously you can only safely call qclear() when you will not use pages allocated earlier again.

n32 qsize() const { return q_le.lsize(); } // page count «

     Method qsize() returns list length: a page count.

explicit yPq(yPBH* heap); // all pages allocated in heap ~yPq(); // qclear() releases all pages to heap (frees refs)

     The constructor needs a source heap and the destructor releases all pages by calling qclear().

struct Qq { yPq const& q_q; Qq(yPq const& q): q_q(q) { } }; « Qq quote() const { return Qq(*this); } // to request dump « void qprint() const; // dump to stdout via yout void qdump(yo& o) const; void qcite(yo& o) const; }; // class yPq (used over yPqm when no hashmap is needed)

     Api just above and below is typical printing api boilerplate similar in style to all other printing methods in þ — please see out and quote demos for detail.

inline yo& operator<<(yo& o, yPq const& x) { « x.qdump(o); return o; } inline yo& operator<<(yo& o, yPq::Qq const& x) { « x.q_q.qdump(o); return o; } inline yo& operator<<(yo& o, yct<yPq> const& x) { « x.c_t.qcite(o); return o; }

yPq.cpp «

     Non-inline yPq methods appear below. Some of this might make more sense after you read list and iter demos.

yPq::yPq(yPBH* heap) : q_H(heap), q_le() { } // no pages «

     The list starts out empty: zero pages inside.

yPq::~yPq() { this->qclear(); q_H = 0; } // nil: stop repeat «

     Destruction releases all pages in the list.

// Each yqnhe is allocated separately -- instead of using // entire pages broken down into yqnhe -- because of two // problems: (1) some uses of yPq will allocate very few pages, // so allocating an entire page of yqnhe's is wasteful; and // (2) we'd be flirting dangerously close to confusing self- // reference if we used our own output to allocate space used // to manage our internal mechanisms, so risk of loopy logic // & bugs would be too high.

     Above is Wil's original comment on yPq's use of individually allocated list elements. It summarizes a weird effect you would see if yPq allocated a page and then put a list handle element inside that page holding the ref to the page's node. Sometimes that sort of thing is perfectly reasonable, even if it makes you feel slightly queasy. Wil decided to do the simple thing instead, appearing in the version shown below.

yP* yPq::qnewP(u16 pid) { // alloc yP owned by yPq; any use « yqnhe* e = 0; if (q_H) e = new(*q_H) yqnhe; if (yunlikely(!e)) { ylog(0, "qnewP() e=nil q_H=%lx\n", (long)q_H); return 0; } yPnh first; yPn* n = q_H->hPn(first, /*eg: id='Pq'*/ pid); if (yunlikely(!n)) { ylog(1, "qnewP() yPn=nil\n"); q_H->hfree(e); return 0; } e->e_nh = n; // put node ref into elem q_le.lpush_back(e); // append elem if (yunlikely(!n->Pngood())) n->Pnbad("yPq::qnewP"); return n->npage(); // return the page inside the node }

     Above you see one yqnhe list elem allocated first, then a yPn page node to go inside. Here the elem is made to ref the new node by assigning it to handle e_hn in the elem. (Follow the link to see a definition.) This causes one extra ref to be added by e_hn, followed by one released ref in first when method qnewP() exits. It would be more efficient to use inline e_hn.hswap(first) to exchange the ref in first with the nil pointer in e_hn, without a more costly net zero change caused by going up and then down again.

     But even better, e_hn can just be passed to hPn() in the first place to receive a first reference. Temp handle first isn't even needed since e_hn is both around already, and exactly where we want the ref to land. Is this clear to you?

     Wil almost changed the code now to remove first and pass e_hn directly to hPn(), but it seemed more educational to leave it like this and include the full explanation so you can think about optimization. Not all equivalent code is equally fast.

void yPq::qclear() { « if (yunlikely(!q_H)) { yH::hnil("yPq::q_H"); return; } yqnhe* e = 0; while ((e = (yqnhe*) q_le.lpop()) != 0) { e->qnheclear(); // release page ref'd by this node handle e->~yqnhe(); // no-op now that handle is already released q_H->hfree(e); // returns space to heap } }

     Note code in qclear() must exactly match memory management in qnewP() shown above, but in reverse. Because each list elem was allocating in the q_H heap, it must be used to free every elem popped from the list.

     The ~yqnhe() destructor called here explicitly would have done the same thing as qnheclear() which does it early: releasing the node from the e_nh handle in the elem. Each page is released, so it goes back to the heap, since presumably there are no other refs to nodes in this q_le list.

void yPq::qprint() const { « yout << yendl; this->qdump(yout); yout << yendl << ynow; }

void yPq::qdump(yo& o) const { « n32 n = q_le.lsize(); if (yunlikely(!n)) { this->qcite(o); return; } // empty? o.oft("<yPq me=%lx H=%lx len=%lu>", (long) this, (long) q_H, (long) q_le.lsize()); unsigned i = 0; char arc[32]; for (yqp p = q_le.lbegin(); p != q_le.lend(); ++p) { ::sprintf(arc, "%u", ++i); const yqnhe* e = (const yqnhe*) (const yqe*) p; if (e) o << yendl << e->e_nh.cite(arc); else o << "<yqnhe e=nil/>"; // how can this happen? } o.ounend("yPq"); }

     Here's another gratuitous use of queue iterator api from the iter demo. Operators used above link to the code.

void yPq::qcite(yo& o) const { « o.of("<yPq me=%lx H=%lx len=%lu/>", (long) this, (long) q_H, (long) q_le.lsize()); }

virtual memory «

     Several years ago Wil wrote part of a buffering system for a server, and was told "don't worry, paging will never happen" because there would always be enough physical memory.

     "Are you sure?" Wil asked. "Because I'd be a lot more careful about alignment and locality if paging might occur." Wil was kinda hoping he'd be allowed to align buffers anyway, but buffer allocation was given as a task to a junior engineer. (Wil avoids meddling.)

     A couple years later the product was tested by a customer who used far less physical memory than logical address space — so paging was heavy: the server suffered and did interesting things.

     In absence of careful new request throttling, it seemed the server strongly preferred to give priority to new requests rather than finish processing old requests. As a result, the address space footprint kept growing — aggravating the paging even further.

     When told about it, Wil noted the observed behavior made sense: new requests were chewing up virgin address space with higher locality in new buffers allocated contiguously, thus giving new request content high memory locality despite no page alignment.

     In contrast, older requests used buffers allocated earlier in time, popped from free lists, and totally lacking in page alignment. The lack of alignment meant any given page had high probability — on average — of splitting a buffer across pages so each buffer shared a page with another buffer, which would easily be part of another request.

     As a result, any request using old buffers was in page contention with other requests since buffers in free lists had random order, or rather the order in which buffers were freed. But new requests allocating buffers in virgin address space had zero contention with other requests, so they won races in ways making the old request queue get bigger.

     What's the moral of this story? If folks like your product, it will get used in more contexts until finally it gets deployed under resource pressure. Whether you design for paging or not, it will get used under virtual memory paging pressure eventually. So you might as well accomodate obvious pain points early: align now.

     Thus yP pages allocated by yPBH heaps are aligned.

map iterators «

     "Can I see a clue to map iterator coding in the right column?" Stu asked. "Remove methods look suggestive."

     "Well, the interesting problem of map element removal is outlined in the mremove() methods," Wil granted.

     "Let me guess," Stu considered. "An iterator must point to a reference to the current map item in the iteration, so it's possible to unlink it from the current bucket. Am I close?"

     "Yes," Wil agreed. "That's how iterating one bucket works. And the iterator can tell if the current element has been removed or not by whether it's already been unlinked."

     "So all that's left is iterating over all the buckets," Stu said. "I don't know why you say this is hard."

     Wil was chagrined. "When did I say it was hard? Don't I go out of my way to say everything is easy?"

     "It gives me the opposite impression," Stu admitted, squinting thoughtfully. "What's wrong with me?"

     "You've been hanging out with liars," Wil explained. "Now you suspect everything is crafty misdirection."

     "That's just what you want me to believe," Stu accused with a pointing finger while smiling at his joke.

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.

page based maps «

     The next two sections below show sample code for page-based hashmaps. The first is ypm for pointer map, which is really a set of pointers because only the keys are represented. And the second is yppm for pointer pair map, which maps a pointer key to a pointer value. Both of them are designed for modest population sizes of no more than several thousands.

     A Lisp pretty printer for toy language support (coming soon) first used these hashmaps to track addresses of container objects already seen and already printed, in order to detect circular references (caused by self-embedding) and to detect structure repetition, both of which can be replaced by a citation instead of literal tree representation.

     However, Wil eventually realized a more complex map supporting a subtle form of copy-on-write was needed in pretty printing for this purpose, to permit two passes over a data structure — one for sizing one for actually printing — without complex self interference. The fancy copy-on-write map will be shown later in the map demo, because it'll be much longer than code shown below. (The tricky parts of the cow map are somewhat entertaining, and not as trivial as the stuff below despite using simple effects.)

     Maps shown below are characteristic of many hashmaps in þ based on fix-sized buffers using pages or books (64K super pages). So the code organization is almost boilerplate for a hashmap that does not resize. (If you add too many members, buckets just get deeper and take linear scans to search.)

     The hashmap style shown below is called open hashing because buckets are open to further element additions. Wil sometimes calls this style open/chaining to emphasize collision resolution is via chaining in a bucket list. Wil prefers open/chaining hashmaps when allocating a large static max capacity closed/probing hashmap is not easy to arrange. Lists in an open/chaining hashmap touch more cache lines, but afford a lot of flexibility and gracefully handles overfilling beyond expected size.

     (Wil has written at least a hundred different hashmap implementations over the years, to suit complex needs in different languages and varying resource constraints and threading contexts. So writing another one always seems no big deal, which of course leads to more of them. Perhaps after you read a few, they'll all start to look the same. It's hard not to write a few for symbol tables and compilers when writing a programming language.)

basic hashing «

     The hash method used is very rudimentary, and probably ought to be replaced with Chongo's (Landon Curt Noll's) FNV hash, named after himself and the original two authors, Glenn Fowler and Phong Vo. A hash based on Fowler Noll Vo should be faster than crc32 and might have better collision resistance.

     This page will updated later when the hash is replaced. Hmm, looks like a hash demo will appear then to show another embodiment of fnv in a þ api.

pointer sets «

     The hashmap shown below represents a set of pointers added as members. All storage is allocated by yPq and consists entirely of yP pages. The hashmap's array is a single page, and each entry added to the map is suballocated from another page divvied up into map elems of type ypm::Mpe, where nested class name Mpe means map pointer element. Another hashmap shown further down in this page has very similar structure, but maps pointer key to pointer value. This map effectively maps a pointer to itself or to nil.

class ypm «

     Class name ypm means thorn pointer map. Here the pointer type is void*, but the map is easily used for any scalar type whose size is not more than sizeof(void*), so Wil also uses ypm to represent integer sets of type ptrdiff_t. (Actually sizeof(ptrdiff_t) can be more than sizeof(void*) — just not the reverse. But this still doesn't stop Wil from pretending they're the same size.)

     Note the minimum footprint of ypm to hold even one pointer member is two pages, or 8K. So only use this ypm when it's not too costly: to hold larger sets or when transiently used. (A more versatile ypm version can start with blocks less than a page.)

class ypm { // pointer hashmap (pages allocated from yPBH) « private: // "small" map where a page of buckets is enough

     The following nested class is the element type:

struct Mpe { // map elem in hashmap bucket « Mpe* e_n; // next elem in hashmap bucket « void* e_p; // pointer key stored in this hashmap elem « Mpe() : e_n(0), e_p(0) { } « Mpe(void* p) : e_n(0), e_p(p) { } Mpe(Mpe* n, void* p) : e_n(n), e_p(p) { } }; // Mpe: elem in list of pointers in one hashmap bucket

     Nested class Mpe is the map pointer element used to add hashmap members. The key (map member) pointer is stored in e_p, and e_n is just a link to the next elem in the same hashmap bucket, where each bucket is a singly linked list.

static const u32 kprime1k = e1i_1021_1k; // prime < 1K « static const u32 kslots = e1i_1021_1k; // prime < 4K page «

     The number of buckets is kslots: the largest prime number less than 1024, because one 4096 byte page can hold at most 1024 Mpe* 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 pages « yPq m_Pq; // pages allocated « // when m_vH is exhausted, we alloc another page for elems: yv m_vH; // space left in current page to alloc elems « void* m_nilval; // pointer value representing nil value « n32 m_n; // count of hashmap entries « Mpe* m_efree; // free list resulting from removals « // m_epp comes from 1st page alloc'd by m_Pq in _minit() Mpe** m_epp; // hashmap array: m_epp[kslots] in a 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 Mpe 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 Mpe elems allocated from m_vH go straight into the m_epp hashmap after construction when new members are added. But any element removed from the hashmap is put into the m_efree element free linked list.

     The current number of map members is m_n — this is the map size. Pointer m_nilval is the value returned by the map on any failed search for a pointer member: it defaults to zero, but can be set to any other value if a map might contain zero as a valid member.

bool _minit(); // true if succeeds 1st init when m_epp=nil Mpe* _mnew(void* p); // alloc new elem from m_vH or new page yo& _mdump_epp(yo& o) const; // dump ptr hashmap

     Follow links above to code and explanatory comments.

void _mpush(Mpe* e) { e->e_n = m_efree; m_efree = e; } «

     When a removed element is freed, _mpush() pushes it on the top of the free list, which allocates in lifo stack order.

Mpe* _mpop() { « Mpe* e = m_efree; if (e) m_efree = e->e_n; return e; }

     A free element is allocated by _mpop() when the free list is not empty. Obviously m_efree must be initialized to nil.

public: n32 mvol() const { // bytes « return (m_Pq.qsize() * yP::kPsz) - m_vH.v_n; }

     The number of bytes used by the map is mvol() if we don't count bytes in the remaining m_vH page yet to be allocated.

void* mremove(void* p); // remove p from map if found void madd(void* p); // add p to map if not present

     Insert and delete are done by madd() and mremove(). Re-adding the same pointer is idempotent — once added, adding it again does nothing: it's still a member. The return of mremove() is m_nilval if key p was not a member.

//fast answer to question: Is address p inside in this map? const void* mfind(const void* p) const; // p or nilval const void* operator[](const void* key) const { « return this->mfind(key); }

     You query map membership with mfind().

// nilval: value returned if no key is found (default zero) explicit ypm(yPBH* heap, void* nilval=0); // heap for pages void mclear(); // releases all pages: empty the map

     The constructor needs an allocating heap and a preferred nil value. Clearing a map removes all members and releases all pages allocated, returning a map to an empty state as if just constructed.

n32 msize() const { return m_n; } // # ptrs in hashmap « ~ypm(); // mclear() releases all pages back to the heap

     Current member count is msize(). Destroying a map releases all pages allocated.

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

     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, ypm const& x) { x.mdump(o); return o; } « inline yo& operator<<(yo& o, ypm::Mq const& x) { x.q_m.mdump(o); return o; } « inline yo& operator<<(yo& o, yct<ypm> const& x) { x.c_t.mcite(o); return o; } «

ypm.cpp «

     Non-inline page methods appear below.

ypm::ypm(yPBH* heap, void* nilval) : m_H(heap), m_Pq(heap) « , m_vH(0,0), m_nilval(nilval), m_n(0), m_efree(0), m_epp(0) { // wait until first call to _mnew() before calling _minit() }

     The constructor makes an empty map. The first page for m_epp is delayed until a member is added.

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

     Destruction empties the map and releases all pages.

void ypm::mclear() { « if (yunlikely(!m_H)) { yH::hnil("ypm::m_H"); return; } m_n = 0; m_efree = 0; // no members or free list elems m_vH.vclear(); m_epp = 0; // buckets allocated again later by _minit() m_Pq.qclear(); // free pages only AFTER any space inside }

     Clearing a map to empty does very little besides free all the pages by clearing the m_Pq list and zeroing m_epp to show lazy init is needed again, just like after construction.

bool ypm::_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 page for nil bucket slots m_epp = (Mpe**)(void*) P; // page as hashmap 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.

ypm::Mpe* ypm::_mnew(void* x) { « // alloc new elem from m_vH, or new page if (yunlikely(!m_H || (!m_epp && !this->_minit()))) return 0; // nothing can be done Mpe* e = this->_mpop(); // see if elem was freed earlier if (e) { e->e_p = x; return e; } // re-use existing elem void* mem = m_vH.vtake(sizeof(Mpe)); // alloc in old page if (!mem) { // must alloc new page broken into elems? yP* P = m_Pq.qnewP(/*pm*/0x706d); if (yunlikely(!P)) { yH::hnil("_mnew"); return 0; } m_vH.vinit(P, yP::kPsz); mem = m_vH.vtake(sizeof(Mpe)); } if (mem) { // raw memory allocated big enough to construct? ++m_n; // we'll add one more member to hashmap return new(mem) Mpe(x); // use placement operator new } return 0; }

     When a new member is added, _mnew() allocs a Mpe map pointer element to hold the new pointer member. First it tries to pop the free list, then it tries to alloc sizeof(Mpe) 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.

void ypm::madd(void* p) { // add p to map if not present « if (yunlikely(!m_H || (!m_epp && !this->_minit()))) return; // nothing can be done Mpe** pp = // bucket for p m_epp + (yh32::hashp(p) % ypm::kslots); for (Mpe* e = *pp; e; e = e->e_n) { // more bucket? if (p == e->e_p) return; // found p in a list elem, return (no-op) } Mpe* e = this->_mnew(p); if (e) { e->e_n = *pp; *pp = e; } // push in bucket }

     When adding a new member, first lazy init is performed by _minit() if needed. Then the bucket to which the pointer hashes is searched — if found, nothing is done. Otherwise a new elem is allocated (either popped from the free list or constructed in new space) and pushed on the top of the bucket list picked by the search.

const void* ypm::mfind(const void* p) const { « // return p if found, else nil if (yunlikely(!m_H || !m_epp)) return 0; // bad, gone, empty: not in map Mpe** pp = m_epp + (yh32::hashp(p) % ypm::kslots); for (Mpe* e = *pp; e; e = e->e_n) { // more bucket? if (p == e->e_p) return p; // found p in list elem, return success } return m_nilval; // not found in list where p would be }

     Finding an old member looks just like madd() except lazy init is not mandatory, and nothing new is added. If the target pointer is not found, mfind() returns the distinguished nil value passed to the constructor, which defaults to zero.

void* ypm::mremove(void* p) { « // remove p if found (return p or nilval) if (yunlikely(!m_H || !m_epp)) return 0; // bad, gone, or empty: not in map n32 i = (yh32::hashp(p) % ypm::kslots); // table index Mpe** pp = m_epp + i; // list bucket for p for (Mpe* e = *pp; e; pp = &e->e_n, e = *pp) { // more list? if (p == e->e_p) { // found p in list elem? unlink elem? *pp = e->e_n; // unlink elem; try to decr member count: if (m_n) --m_n; else yellf(__LINE__,__FILE__,"mremove() underflow"); this->_mpush(e); return p; // tell caller removal actually happened } } return m_nilval; // not found in list where p would be }

     Removing an old pointer member also looks like madd() and mfind() except instead of letting pp point to the bucket list head, it follows the list traversal down, always pointing at the next element to be examined. If the pointer is found, it's element is unlinked just by updating pp, and then the element is pushed in the free list.

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

void ypm::mdump(yo& o) const { « if (!m_n) { this->mcite(o); return; } // empty? o.oft( "<ypm me=%lx H=%lx epp=%lx n=%u Plen=%u vn=%u free=%lx>", (long) this, (long) m_H, (long) m_epp, (int) m_n, (int) m_Pq.qsize(), (int) m_vH.v_n, (long) m_efree); o << yendl << m_Pq.quote(); // print queue of pages o << yendl; this->_mdump_epp(o); o.ounend("ypm"); }

     The only slightly interesting part of printing is hashmap scan shown below in _mdump_epp().

void ypm::mcite(yo& o) const { « o.of( "<ypm me=%lx H=%lx epp=%lx n=%u Plen=%u vn=%u free=%lx/>", (long) this, (long) m_H, (long) m_epp, (int) m_n, (int) m_Pq.qsize(), (int) m_vH.v_n, (long) m_efree); }

yo& ypm::_mdump_epp(yo& o) const { « int sz = 0; o.oft("<map vec=%lx slots=%d size=%d> " "<!-- sizeof(Mpe)=%d -->", (long) m_epp, (int) kslots, (int) m_n, sizeof(Mpe)); for (unsigned i = 0; m_epp && i < kslots; ++i) { Mpe* p = m_epp[i]; if (p) { o.on(); // anything in this bucket? if (p->e_n) { // more than one in list? // print N nodes inside a list tag? o.oft("<bucket i=%d>", (int) i); // slot index in tag n32 len = 0; for ( ; p; p = p->e_n) { if (++len > 1) o << yendl; ++sz; o.of("<e p=%#lx/>", (long) p->e_p); } o.ou(); // trailing count: o.of("</bucket> <!-- %d -->", (int) sz); } else { // otherwise print node by itself o.of("<e p=%#lx/> <!-- %d i=%d -->", (long) p->e_p, ++sz, (int) i); } } } o.ounend("map"); return o; }

     The map buckets are printed above with annotation showing the bucket index in the m_epp array so distribution can be seen. When a bucket contains only one elem, the handle inside is printed by itself. Otherwise a list of length over one is printed inside a tag grouping bucket collisions.

pointer pair maps «

     The hashmap shown below is extremely similar to ypm shown in the last section, which you ought to study first because fewer comments will be given below.

     Once again, all storage is allocated by yPq and consists entirely of yP pages. The hashmap's array is a single page, and each entry added to the map is suballocated from another page divvied up into map elems of type ypm::Mkve, where nested class name Mkve means map key value element. Each member is a pair (key, val) of pointers.

class yppm «

     Class name yppm means thorn pointer pair map (or more literally: pointer pointer map). As before in ypm, the pointer type is void*, but the map is easily used for any scalar type whose size is not more than sizeof(void*).

class yppm { // ptr PAIR hashmap (pages allocated from yPBH) « private: // "small" map where a page of buckets is enough

struct Mkve { // map key/val elem in hashmap bucket « Mkve* e_n; // next elem in hashmap bucket « void* e_key; // pointer key stored in hashmap elem « void* e_val; // the value associated with key « Mkve() : e_n(0), e_key(0), e_val(0) { } « Mkve(Mkve* n, void* k, void* v) : e_n(n), e_key(k), e_val(v) { } Mkve(void* k, void* v) : e_n(0), e_key(k), e_val(v) { } }; // Mkve: elem in list of ptr pairs in hashmap bucket

     Nested class Mkve is the key/value pair element used to add map members. The key pointer is e_key, the value is e_key, and once again e_n is just a link to the next elem in the same hashmap bucket, where each bucket is a singly linked list.

static const u32 kprime1k = e1i_1021_1k; // prime < 1K « static const u32 kslots = e1i_1021_1k; // prime < 4K «

     Just as in ypm shown earlier, the definition assumes a page is 4K in size and that pointers are 32-bit, so a page can contain at most 1024 pointers. The map has kslots buckets because 1021 is the biggest prime under 1024 — primes are good hashmap sizes.

yPBH* m_H; // source of all pages « yPq m_Pq; // pages allocated « // when m_vH is exhausted, we alloc another page for elems: yv m_vH; // space left in current page to alloc elems « void* m_nilval; // pointer value representing nil value « n32 m_n; // count of hashmap entries « Mkve* m_efree; // free list resulting from removals « // m_epp comes from 1st page alloc'd by m_Pq in _minit(): Mkve** m_epp; // hashmap array: m_epp[kslots] in a page «

     All state members have the same meaning as those in ypm shown earlier, except the elem type is Mkve:

     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 Mkve 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 Mkve elems allocated from m_vH go straight into the m_epp hashmap after construction when new members are added. But any element removed from the hashmap is put into the m_efree element free linked list.

     The current number of map members is m_n — this is the map size. Pointer m_nilval is the value returned by the map on any failed search for a pointer member: it defaults to zero, but can be set to any other value if a map might contain zero as a valid member.

bool _minit(); // true if succeeds 1st init when m_epp=nil Mkve* _mnew(void* k, void* v); // alloc in m_vH or new page yo& _mdump_epp(yo& o) const; // dump pointer pair hashmap

void _mpush(Mkve* e) { e->e_n = m_efree; m_efree = e; } «

     Freed elems are pushed on the free list by _mpush().

Mkve* _mpop() { « Mkve* e = m_efree; if (e) m_efree = e->e_n; return e; }

     Old free elems are allocated when _mpop() pops the free list.

public: typedef ptrdiff_t p_t; // for void* pointers as integers «

n32 mvol() const { // bytes of "volume" -- size « return (m_Pq.qsize() * yP::kPsz) - m_vH.v_n; }

void* mremove(void* key); // cut map key (return old val) void* madd(void* key, void* val); // + pair (return old val) void* madd(p_t key, void* val) { return this->madd((void*)key, val); }

     As in yom, and delete are done by madd() and mremove(). But now adding the same key twice is not idempotent because the value can change. If madd() updates an old pair, the old value is returned; otherwise the distinguished nil value is returned.

/// \return value for key, or nil (assumes value never nil) void* mfind(const void* key) const; // key val or nilval void* operator[](const void* key) const { « return this->mfind(key); } void* operator[](p_t key) const { return this->mfind((void*)key); }

// nilval: value returned if no key is found (default zero) explicit yppm(yPBH* heap, void* nilval=0); // heap for pages void mclear(); // release all pages: make map empty

     The constructor needs an allocating heap and distiguished nil value defaulting to zero. Clearing a heap makes it empty as if just constructed.

n32 msize() const { return m_n; } // # ptrs in hashmap « ~yppm(); // mclear() to releases all pages back to heap

     A count of current map members is returned by msize(). Destruction empties the map and releases all pages.

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

     Printing boilerplate above and below are shown in out and quote demos; sample use appears in other demos.

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

yppm.cpp «

     Non-inline page methods appear below.

yppm::yppm(yPBH* heap, void* nilval) : m_H(heap), m_Pq(heap) « , m_vH(0,0), m_nilval(nilval), m_n(0), m_efree(0), m_epp(0) { // wait until first call to _mnew() before calling _minit() }

     The constructor makes an empty map. The first page for m_epp is delayed until a member is added.

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

void yppm::mclear() { « if (yunlikely(!m_H)) { yH::hnil("yppm::m_H"); return; } m_n = 0; m_efree = 0; // no members or free list elems m_vH.vclear(); m_epp = 0; // buckets allocated again later by _minit() m_Pq.qclear(); // free pages only AFTER any space inside }

     Clearing a map to empty does little besides free all pages by clearing the m_Pq list and zeroing m_epp to show lazy init is needed again, as after construction.

bool yppm::_minit() { « // true if succeeds 1st 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 page for nil bucket slots m_epp = (Mkve**)(void*) P; // page as hashmap bucket array } return true; // buckets ready to use }

     If not done already, _minit() allocs a first zero page for a m_epp map consisting of all empty buckets.

yppm::Mkve* yppm::_mnew(void* k, void* v) { « // new elem in m_vH, or new page if (yunlikely(!m_H || (!m_epp && !this->_minit()))) return 0; // nothing can be done Mkve* e = this->_mpop(); // see if elem was freed by earlier if (e) { // re-use existing elem? e->e_key = k, e->e_val = v; return e; } void* mem = m_vH.vtake(sizeof(Mkve)); // alloc in old page if (!mem) { // must alloc new page broken into elems? yP* P = m_Pq.qnewP(/*pm*/0x706d); if (yunlikely(!P)) { yH::hnil("_mnew"); return 0; } m_vH.vinit(P, yP::kPsz); mem = m_vH.vtake(sizeof(Mkve)); } if (mem) { // raw memory allocated big enough to construct? ++m_n; // we'll add one more member to hashmap return new(mem) Mkve(k, v); // use placement operator new } return 0; }

     When a new member is added, _mnew() allocs a Mkve map pointer element to hold the new pointer member. First it tries to pop the free list, then it tries to alloc sizeof(Mkve) 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.

     All remaining yppm methods are nearly identical to ypm methods shown in the last section, except all the elems are Mkve pairs instead of Mpe single pointer keys.

void* yppm::madd(void* k, void* v) { « // add (key,val) to map (return old val) if (yunlikely(!m_H || (!m_epp && !this->_minit()))) return m_nilval; // nothing doable n32 i = (yh32::hashp(k) % yppm::kslots); // index in table Mkve** pp = m_epp + i; // bucket for k for (Mkve* e = *pp; e; e = e->e_n) { // more bucket? if (k == e->e_key) { // found p in list, return old val? void* oldval = e->e_val; e->e_val = v; return oldval; } } Mkve* e = this->_mnew(k, v); if (e) { e->e_n = *pp; *pp = e; } // push bucket return m_nilval; // no old value to return }

void* yppm::mfind(const void* p) const { « // return val if key is in map if (yunlikely(!m_H || !m_epp)) return 0; // bad, gone, empty: not in map n32 i = (yh32::hashp(p) % yppm::kslots); // table index Mkve** pp = m_epp + i; // bucket for p for (Mkve* e = *pp; e; e = e->e_n) { // more bucket? if (p == e->e_key) return e->e_val; // found p in list elem, return val } return m_nilval; // not found in list where p would be }

void* yppm::mremove(void* p) { « // remove p if found (return p or nilval) if (yunlikely(!m_H || !m_epp)) return 0; // bad, gone, or empty: not in map n32 i = (yh32::hashp(p) % yppm::kslots); // table index Mkve** pp = m_epp + i; // list bucket for p for (Mkve* e = *pp; e; pp = &e->e_n, e = *pp) { // more list? if (p == e->e_key) { // found p in list elem? unlink elem? *pp = e->e_n; // unlink elem; try to decr member count: if (m_n) --m_n; else yellf(__LINE__,__FILE__,"mremove() underflow"); this->_mpush(e); return e->e_val; // tell caller removal happened } } return m_nilval; // not found in list where p would be }

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

void yppm::mdump(yo& o) const { « if (!m_n) { this->mcite(o); return; } // empty? o.oft( "<yppm me=%lx H=%lx epp=%lx n=%u Plen=%u vn=%u free=%lx>", (long) this, (long) m_H, (long) m_epp, (int) m_n, (int) m_Pq.qsize(), (int) m_vH.v_n, (long) m_efree); o << yendl << m_Pq.quote(); // print queue of pages o << yendl; this->_mdump_epp(o); o.ounend("yppm"); }

void yppm::mcite(yo& o) const { « o.of( "<yppm me=%lx H=%lx epp=%lx n=%u Plen=%u vn=%u free=%lx/>", (long) this, (long) m_H, (long) m_epp, (int) m_n, (int) m_Pq.qsize(), (int) m_vH.v_n, (long) m_efree); }

yo& yppm::_mdump_epp(yo& o) const { « int sz = 0; o.oft("<map vec=%lx slots=%d size=%d> " "<!-- sizeof(Mkve)=%d -->", (long) m_epp, (int) kslots, (int) m_n, sizeof(Mkve)); for (unsigned i = 0; m_epp && i < kslots; ++i) { Mkve* p = m_epp[i]; if (p) { o.on(); // anything in this bucket? if (p->e_n) { // more than one in list? // print N nodes inside a list tag? o.oft("<bucket i=%d>", (int) i); // tag slot index n32 len = 0; for ( ; p; p = p->e_n) { if (++len > 1) o << yendl; ++sz; o.of("<e key=%lx val=%lx/>", (long) p->e_key, (long) p->e_val); } o.ou(); // trailing count: o.of("</bucket> <!-- %d -->", (int) sz); } else { // otherwise print node by itself o.of("<e key=%lx val=%lx/> <!-- %d i=%d -->", (long) p->e_key, (long) p->e_val, ++sz, (int) i); } } } o.ounend("map"); return o; }