Þ   briarpig  » thorn  » demos  » heap


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

problem

     Instead of standard heap apis — new() or malloc() — Wil sometimes needs a heap abstraction he can subclass and replace at need. Wil's default yH heap implementation uses malloc() and free(), but subclasses can do as they please.

     Wil also sometimes replaces global operator new() and operator delete() in all code using them. This is useful for debugging and analysis. Also, in anti-C++ shops pursuing C and/or forbidding C++ standard libraries, Wil can still write code in C++ by replacing heap operators with calls to malloc() and free() (or whatever a given environment prefers). So this page also shows how to replace new() and delete() if you can recompile all sources involved; but it's awkward with third party libraries.

     Wil's main heap for dynamic languages only incidentally uses heap yH's simple base api. To control where memory is allocated for garbage collection — and to prevent fragmentation, Wil uses a complex subclass of yH introduced in (future) page and book demos, allocating space in page and super-page granularity. The page/book heap in leaf class yPBH actually uses root class yH as a raw allocator whose blocks are suballocated by yPBH.

base

     The main api of yH is only three methods hmalloc(), hcalloc(), and hfree() which simply call the similarly named standard C library methods in the default method implementations. Subclasses must aim for the same contract: hcalloc() zeroes allocated memory and hfree() deallocates anything allocated by either hmalloc() or hcalloc(). Anything else is extra.

     Non-virtual hcopy() utility methods call virtual hmalloc() to get memory for copying yv runs (cf «). Each subclass might or might not be threadsafe. Any instance of yH is threadsafe in so far as it has no state, and the standard C heap is itself threadsafe.

class yH { // heap allocator using ::malloc() and ::free() « public: static void hnil(const char* slot); // call on nil slot seen static yH g_1H; // singleton global instance of yH yH() { } virtual ~yH(); virtual void* hmalloc(n32 size); // default is malloc(size) virtual void* hcalloc(n32 n, n32 size); // ::calloc(n, size) virtual void hfree(void* p); // default is ::free(p) // each hcopy() method allocates one more byte for final nul // new copy of one using hmalloc(one.v_n+1) & final end nul yv hcopy(yv const& one); // v_n in return equals one.v_n // new copy of both one & two appended, w/ trailing end nul yv hcopy(yv const& one, yv const& two); // one.v_n+two.v_n }; // class yH

     Although yH has an abstract api, the class itself is concrete: you can instantiate yH itself and it allocates memory using malloc() and free(). Singleton instance yH::g_1H is provided so you need not instantiate any other instance of yH; one is enough:

yH yH::g_1H; // convenience singleton global instance of yH yH::~yH() { } // empty «

     The destructor does nothing since yH has no state. And the three main virtual methods are trivial wrappers:

void* yH::hmalloc(n32 size) { // base is ::malloc(size) « return ::malloc(size? size: 1); // at least one byte } void* yH::hcalloc(n32 n, n32 size) { // ::calloc(n, size) « return ::calloc(n? n: 1, size? size: 1); // at least 1 byte } void yH::hfree(void* p) { // default is ::free(p) « if (p) ::free(p); // noop if p is nil }

     Method hnil() only logs unexpected nil slots:

/*static*/ void yH::hnil(const char* slot) { // call when nil is seen in slot « if (!slot) slot = "?"; ylog(1, "yH::hnil(slot=%s) NIL\n", slot); // backtrace later }

     The remaining hcopy() methods provide gratuitous convenience support for copying contiguous octet sequences in yv iovec format from the run demo. The main idea of hcopy() is to ease writing some kinds of sample code that copies iovec buffers, when careful memory allocation is less important than getting immediate, clear results. Done early in one place, this reduces clutter later.

/// copy of one using hmalloc(one.v_n+1) + final end nul yv yH::hcopy(yv const& one) { // v_n return: equals one.v_n « n32 n = one.v_n; if (one.v_p && n) { u8* p = (u8*) this->hmalloc(n+1); yv copy(p, n); if (p) { ::memcpy(p, one.v_p, n); p[n] = 0; // trailing nul } return copy; } return one; // no alloc if nil one.v_p; just bitwise copy } /// new copy of both one and two appended, + trailing end nul yv yH::hcopy(yv const& one, yv const& two) { n32 n = one.v_n + two.v_n; // v_n return: one.v_n+two.v_n if (n && (one.v_p || two.v_p)) { // at least one is non-nil? u8* p = (u8*) this->hmalloc(n+1); yv copy(p, n); if (p) { if (one.v_n && one.v_p) // anything to copy from one? ::memcpy(p, one.v_p, one.v_n); if (two.v_n && two.v_p) // anything to copy from two? ::memcpy(p + one.v_n, two.v_p, two.v_n); p[n] = 0; // trailing nul } return copy; } yv nominal(0, n); // just sum two lengths if both are nil return nominal; // no alloc if both nil; just sum length }

     Each memory block allocated by hcopy() has one extra byte of space to hold a nul byte; but this extra byte is not reflected in the v_n length of the yv instance returned, because the extra nul is not part of the copied content. (It's there only for debugging, to make examination under a debugger like gdb a better experience given a friendly C string termination for display purposes.)

     Both versions of hcopy() aim for constructive behavior when a nil pointer is encountered: in this case, the yv returned also has a nil v_p pointer but sums the input v_n lengths.

example subclass

     The following yH subclass shows a random assortment of effects you might choose to use in heap subclasses. The name yxHH is somewhat arbitrary since Wil wrote it as a throw-away example for this demo (and for use in other demos where effects of heap allocation can be shown by printing yxHH).

     Because yxHH has state, Wil added a yx instance from the mutex demo to show how you might make a heap threadsafe, even though Wil won't write any samples using yxHH with more than one thread.

     Member h_stats sees average and std deviation for allocated block sizes, illustrating yrsum from the stat demo.

     An explanation of nested class Hq and inline quote() appear in the quote demo. And the api for printing is shown in the out demo. (But you needn't read those first.)

     As you read the code, try to remember Wil included random features that might look interesting, without following a theme or exercising restraint to increase simplicity. The code is just a big pile of stuff intended to provoke ideas, to suggest similar effects you might pursue in other contexts.

#define yxHH_SUM_BLOCK_PREFIX 1 /*8-byte size+tag prefix« */ class yxHH : public yH { // mutexed heap sums block sizes « protected: // mutex added just for extra demo feature in sample heap: mutable yx h_mutex; // assuming we need thread safety yH* h_heap; // OPTIONAL heap used only if given yrsum h_stats; // (N, avg, sdv) for all allocations unsigned h_n; // current allocated block count #ifdef yxHH_SUM_BLOCK_PREFIX unsigned h_sum; // just sum of allocated block sizes #endif /*yxHH_SUM_BLOCK_PREFIX*/

     If yxHH_SUM_BLOCK_PREFIX is defined, every block allocated starts with an extra eight byte prefix containing the block size and a magic signature. The point of this compiler switch is to show you can add or omit features depending on build options you need for debugging.

yxHH(); // allocate using malloc(), free(), and calloc() yxHH(yH* parent); // allocate using parent heap virtual ~yxHH(); virtual void* hmalloc(n32 size); // indirectly malloc(size) virtual void* hcalloc(n32 n, n32 sz); // ::calloc(n, sz) virtual void hfree(void* p); // indirectly ::free(p)

     This part of the api shows yH subclassing. Note a parent heap for the constructor is optional; when not provided, malloc() and free() are used instead.

     The following global singleton instance of yxHH is used by sample code in the right column for ymu_malloc() and ymu_free() (cf »).

static yxHH g_1xHH; // global singleton instance of yxHH

     Struct Hprefix is only used as an eight byte prefix of every block allocated if yxHH_SUM_BLOCK_PREFIX is defined.

enum { e_Htag = 0x79784848 }; // 'yxHH' as prefix tag « struct Hprefix { // used if yxHH_SUM_BLOCK_PREFIX « n32 p_size; // size of allocated block n32 p_tag; // must equal yxHH::e_Htag Hprefix() : p_size(0), p_tag(yxHH::e_Htag) { } Hprefix(n32 sz) : p_size(sz), p_tag(yxHH::e_Htag) { }

void pinit(n32 sz) { // late construction « p_size = sz; p_tag = yxHH::e_Htag; } };

     Api shown below is the usual sort of boilerplate used for printing as shown in out and quote demos.

// debug printing this heap is a main reason for yxHH: struct Hq { yxHH const& q_H; // value for quote() « Hq(yxHH const& h): q_H(h) { } }; Hq quote() const { return Hq(*this); } // request dump « void Hprint() const; // dump to stdout via youts void Hdump(yo& o) const; void Hcite(yo& o) const; }; // class yxHsum inline yo& operator<<(yo& o, yxHH const& x) { x.Hdump(o); return o; } inline yo& operator<<(yo& o, yxHH::Hq const& x) { x.q_H.Hdump(o); return o; } inline yo& operator<<(yo& o, yct<yxHH> const& x) { x.c_t.Hcite(o); return o; }

     Here's where the singleton is instantiated:

yxHH yxHH::g_1xHH; // global singleton instance of yxHH «

     Constructing yxHH mainly involves initializing h_mutex and h_stats. The destructor logs if outstanding blocks exist.

yxHH::yxHH() // alloc using malloc(), free(), & calloc() « : h_mutex(), h_heap(0), h_stats(), h_n(0) #ifdef yxHH_SUM_BLOCK_PREFIX , h_sum(0) // sum of unfreed block sizes when we track #endif /*yxHH_SUM_BLOCK_PREFIX*/ { } yxHH::yxHH(yH* parent) // allocate using parent heap : h_mutex(), h_heap(parent), h_stats(), h_n(0) #ifdef yxHH_SUM_BLOCK_PREFIX , h_sum(0) // sum of unfreed block sizes when we track #endif /*yxHH_SUM_BLOCK_PREFIX*/ { } yxHH::~yxHH() { « if (h_n) { // some blocks not yet freed? yellf(__LINE__,__FILE__, "yxHH::~yxHH() h_n=%u blocks remain", (unsigned) h_n); } }

     All the printing methods here just call Hcite(). You can see examples of output in the right column (cf »).

void yxHH::Hprint() const { « yout << yendl; this->Hcite(yout); yout << yendl << ynow; } void yxHH::Hdump(yo& o) const { « this->Hcite(o); } void yxHH::Hcite(yo& o) const { « unsigned sum = 0; #ifdef yxHH_SUM_BLOCK_PREFIX sum = h_sum; // sum of all unfreed block sizes if tracked #endif /*yxHH_SUM_BLOCK_PREFIX*/ o.of("<yxHH all=%llu avg=%3.1f sdv=%3.1f n=%u sum=%u/>", (long long) h_stats.size(), (double) h_stats.avg(), (double) h_stats.sdv(), (unsigned) h_n, (unsigned) sum); }

     See bottom column right for hmalloc(), hcalloc(), and hfree() implementations (cf »).

     Further use of yxHH will appear in the (future) iter demo to reveal effects of list mutation on allocation heaps.

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.

new and delete

     Some folks are aware you can override operator new() and operator delete(), but don't realize to what extremes this can be taken. You can also replace standard versions provided by the standard C++ library, by declaring inline replacements before the operators are first used.

     However, there are so many gotchas it's hard to list them all — and frankly it's tedious to explain each one. For example, suppose you use a third party library that's already compiled; if the api allocates objects you must delete, or vice versa, then you're obligated (awkwardly) to honor the library's original contract: that you'll share a standard definition of heap operators. This can be hard to work around. (You must preseve the ability to reach the original new() and delete() for that library.)

     Ignoring complex problems like that, Wil usually has a header file named something like ynew.h which is included first before any other header file in each of his header files and source files. This header contains declarations you see below. Whether you can succeed in using variants without reference to exceptions like std::bad_alloc and std::nothrow_t depends on whether you include any system header including a standard C++ new.h header. Once a standard header declares operators one way, you can't declare a new signature without a conflict.

     If you write an embedded system using no standard C++ headers at all, you can easily use the simpler versions of new() and delete() inlines below with no mention of exceptions. Otherwise you must leave switch YNEW_STD_BAD_ALLOC defined to agree with the usual fare.

// stdlib.h: size_t, malloc(), free() #include <stdlib.h> #define YNEW_STD_BAD_ALLOC 1 /*for std:: compatible decls « */ #ifdef YNEW_STD_BAD_ALLOC #include <new> #endif /*YNEW_STD_BAD_ALLOC*/

     All variants of new() and delete() and below are defined in terms of these two methods mimicking malloc() and free():

void* ymu_malloc(size_t size); // perhaps just malloc() void ymu_free(void* ptr); // perhaps just free()

     You can instead use malloc() and free(), which also works fine. But instead we'll use ymu_malloc() and ymu_free() to show some games you can play. In order to maximize applicability to the yH api shown on this page, we'll define those in terms of a yH subclass even though this isn't necessary. So dependency on yH is just an option, done gratuitously in ymu_malloc() and ymu_free() as illustration.

     Okay, here are inlines with exception types in signatures:

#ifdef YNEW_STD_BAD_ALLOC inline void* operator new(size_t n) throw (std::bad_alloc) { « return ymu_malloc(n); } inline void* operator new(size_t n, const std::nothrow_t&) throw (std::bad_alloc) { return ymu_malloc(n); } inline void* operator new[](size_t n) throw (std::bad_alloc) { « return ymu_malloc(n); } inline void* operator new[](size_t n, const std::nothrow_t&) throw (std::bad_alloc) { return ymu_malloc(n); } inline void operator delete(void* p) { « return ymu_free(p); } inline void operator delete[](void* p) { « return ymu_free(p); } // A comp.lang.c++ FAQ says these can be called on our behalf: inline void operator delete(void* p, const std::nothrow_t&) { return ymu_free(p); } inline void operator delete[](void* p, const std::nothrow_t&) { return ymu_free(p); } #endif /*YNEW_STD_BAD_ALLOC*/

     Since there are fewer versions without exception types, the following list of inlines is much shorter:

#ifndef YNEW_STD_BAD_ALLOC inline void* operator new(size_t n) { « return ymu_malloc(n); } inline void* operator new[](size_t n) { « return ymu_malloc(n); } inline void operator delete(void* p) { « return ymu_free(p); } inline void operator delete[](void* p) { « return ymu_free(p); } #endif /*YNEW_STD_BAD_ALLOC*/

     Note you can use the same method for vector and non-vector versions of operators, just as shown above using ymu_malloc() and ymu_free() everywhere regardless of whether an array is allocated or not. Or instead, you could choose to preserve the distinction, and use different method for vectors. For example, if you need vector specific statistics, you can preserve that information.

     Finally, here's the code for ymu_malloc() and ymu_free(). But note comments just afterward explaining this kludge.

void* ymu_malloc(size_t sz) { // example malloc() replacement « return mu::yxHH::g_1xHH.hmalloc(sz); } void ymu_free(void* p) { // debug example free() replacement « return mu::yxHH::g_1xHH.hfree(p); }

     The guts of ymu_malloc() and ymu_free() could just as easily be malloc() and free() — or for that matter, direct calls could be used instead. So what's up with heap yxHH shown above?

     Class yxHH is an example subclass of yH shown bottom column left (cf «), and it's a collection of random debugging tactics you might use when implementing a heap subclass. The yxHH name is somewhat random because it's a throw-away example as far as Wil is concerned, written specifically for this demo. However, Wil added the ability to print yxHH so you can see usage statistics including count, average, and standard deviation (for all allocations) plus the N currently allocated blocks with a sum of block sizes.

     The following sample code shows how the effect of inlines for operator new() and operator delete() can be seen as state changes in yxHH when printed. (In these examples, each allocated block begins with an eight byte prefix, so all the sizes are eight bytes more than you might otherwise expect.)

yout << yendl; « yout << "# heap before:" << yendl; yout << yxHH::g_1xHH.quote() << yendl; n32* narray = new n32[8]; yout << "# heap after new[8] n32:" << yendl; yout << yxHH::g_1xHH.quote() << yendl; n32* narray2 = new n32[12]; yout << "# heap after new[12] n32:" << yendl; yout << yxHH::g_1xHH.quote() << yendl; delete [] narray; yout << "# heap after delete [] narray:" << yendl; yout << yxHH::g_1xHH.quote() << yendl; delete [] narray2; yout << "# heap after delete [] narray2:" << yendl; yout << yxHH::g_1xHH.quote() << yendl; yout << ynow; // flush yout to stdout

     The printed output appears below. Each yxHH tag was written by yxHH::Hdump() (cf «). Note the last line shows total outstanding space allocation returns to zero after both arrays are deleted.

# heap before: <yxHH all=0 avg= 0.0 sdv=0.0 n=0 sum=0/> # heap after new[8] n32: <yxHH all=1 avg=40.0 sdv=0.0 n=1 sum=40/> # heap after new[12] n32: <yxHH all=2 avg=48.0 sdv=11.3 n=2 sum=96/> # heap after delete [] narray: <yxHH all=2 avg=48.0 sdv=11.3 n=1 sum=56/> # heap after delete [] narray2: <yxHH all=2 avg=48.0 sdv=11.3 n=0 sum=0/>

     Wil actually wrote the yxHH heap first to show the effects of using template queue yqt in the list demo, along with the element erasure using template iterators in the (future) iter demo.

yxHH allocation

     The following kitchen sink implementations of hmalloc(), hcalloc(), and hfree() are somewhat explained by the left column introduction to yxHH (cf «). They appear here to balance the left and right columns. (Otherwise a long dialog would be necessary, and it's late already.)

void* yxHH::hmalloc(n32 size) { // base is ::malloc(size) « yxg guard(h_mutex); if (!size) size = 1; // at least one byte #ifdef yxHH_SUM_BLOCK_PREFIX size += sizeof(yxHH::Hprefix); // leading per-block prefix void* p = (h_heap)? h_heap->hmalloc(size) : ::malloc(size); yxHH::Hprefix* prefix = (yxHH::Hprefix*) p; if (p) { prefix->pinit(size); h_sum += size; p = prefix + 1; // skip leading yxHH::Hprefix instance } #else /*yxHH_SUM_BLOCK_PREFIX*/ void* p = (h_heap)? h_heap->hmalloc(size) : ::malloc(size); #endif /*yxHH_SUM_BLOCK_PREFIX*/ if (p) { h_stats += size; // track (N, avg, sdv) stats ++h_n; } return p; }

     Of course hcalloc() looks like hmalloc() with small edits:

void* yxHH::hcalloc(n32 n, n32 sz) { // ::calloc(n, sz) « yxg guard(h_mutex); n32 size = n * sz; if (0 == size) { // either n or sz is zero? n = sz = size = 1; // at least one byte } #ifdef yxHH_SUM_BLOCK_PREFIX size += sizeof(yxHH::Hprefix); // leading per-block prefix void* p = (h_heap)? h_heap->hcalloc(n, sz) : ::calloc(n, sz); yxHH::Hprefix* prefix = (yxHH::Hprefix*) p; if (p) { prefix->pinit(size); h_sum += size; p = prefix + 1; // skip leading yxHH::Hprefix instance } #else /*yxHH_SUM_BLOCK_PREFIX*/ void* p = (h_heap)? h_heap->hcalloc(n, sz) : ::calloc(n, sz); #endif /*yxHH_SUM_BLOCK_PREFIX*/ if (p) { h_stats += size; // track (N, avg, sdv) stats ++h_n; } return p; }

     If block sizes are tracked, hfree() asserts the tag in the block's prefix is right, and the size of that block is subtracted from the outstanding sum of all blocks allocated.

void yxHH::hfree(void* p) { // default is ::free(p) « if (p) { yxg guard(h_mutex); #ifdef yxHH_SUM_BLOCK_PREFIX yxHH::Hprefix* prefix = ((yxHH::Hprefix*) p) - 1; if (e_Htag == prefix->p_tag) prefix->p_tag = 0xdeadbeef; else { // wrong tag?? ylog(0, "yxHH::hfree() p_tag=%#x != e_Htag=%#x", (int) prefix->p_tag, (int) e_Htag); yassert(e_Htag == prefix->p_tag); // die } if (h_sum >= prefix->p_size) { // can subtract all? h_sum -= prefix->p_size; // less unfreed total size } else { // this block larger than remaining sum?? ylog(0, "hfree() size=%d exceeds sum=%d", (int) prefix->p_size, (int) h_sum); h_sum = 0; } p = prefix; // prefix start is the actual block to free #endif /*yxHH_SUM_BLOCK_PREFIX*/ if (h_n) // we still show a block is allocated? --h_n; // one block less now extant else { yellf(__LINE__,__FILE__,"yxHH::hfree() h_n=0 underflow"); } if (h_heap) // heap was used to alloc all blocks? h_heap->hfree(p); else ::free(p); } }

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. No feeds or repackaging is allowed. You can link this page if you want folks to read it.