diff --git a/changelog/forkgc.dd b/changelog/forkgc.dd new file mode 100644 index 0000000000..90eba6743a --- /dev/null +++ b/changelog/forkgc.dd @@ -0,0 +1,16 @@ +A concurrent GC for Posix systems + +For Posix systems that support the fork() function (or the clone() on linux systems), +the conservative/precise GC can be made concurrent by enabling the 'fork' GC options in the usual ways, e.g. +by adding `--DRT-gcopt=fork:1` to the command line or by embedding +---- +extern(C) __gshared string[] rt_options = [ "gcopt=fork:1" ]; +---- +into your linked binary (see $(LINK $(ROOT_DIR)spec/garbage.html#gc_config)). + +The application continues execution and new memory is allocated from the system while the forked +process is marking heap objects. Parallel marking is disabled for the forked process so it only +uses a single thread for minimal impact on the concurrent execution of the application. + +This reduces "stop the world" time at the cost of needing more memory and page-protection overhead when +writing to memory currently being scanned. diff --git a/src/core/exception.d b/src/core/exception.d index 734fbcf9ba..4a812e88b2 100644 --- a/src/core/exception.d +++ b/src/core/exception.d @@ -453,6 +453,18 @@ unittest } +/** +* Thrown on a configuration error. +*/ +class ForkError : Error +{ + this( string file = __FILE__, size_t line = __LINE__, Throwable next = null ) @nogc nothrow pure @safe + { + super( "fork() failed", file, line, next ); + } +} + + /** * Thrown on a switch error. */ @@ -716,6 +728,22 @@ extern (C) void onInvalidMemoryOperationError(void* pretend_sideffect = null) @t throw staticError!InvalidMemoryOperationError(); } + +/** + * A callback for errors in the case of a failed fork in D. A $(LREF ForkError) will be thrown. + * + * Params: + * file = The name of the file that signaled this error. + * line = The line number on which this error occurred. + * + * Throws: + * $(LREF ConfigurationError). + */ +extern (C) void onForkError( string file = __FILE__, size_t line = __LINE__ ) @trusted pure nothrow @nogc +{ + throw staticError!ForkError( file, line, null ); +} + /** * A callback for unicode errors in D. A $(LREF UnicodeException) will be thrown. * diff --git a/src/core/gc/config.d b/src/core/gc/config.d index 1879ecbe1b..258183fd50 100644 --- a/src/core/gc/config.d +++ b/src/core/gc/config.d @@ -15,6 +15,7 @@ __gshared Config config; struct Config { bool disable; // start disabled + bool fork = false; // optional concurrent behaviour ubyte profile; // enable profiling with summary when terminating program string gc = "conservative"; // select gc implementation conservative|precise|manual @@ -39,8 +40,9 @@ struct Config printf("GC options are specified as white space separated assignments: disable:0|1 - start disabled (%d) + fork:0|1 - set fork behaviour (%d) profile:0|1|2 - enable profiling with summary when terminating program (%d) - gc:".ptr, disable, profile); + gc:".ptr, disable, fork, profile); foreach (i, entry; registeredGCFactories) { if (i) printf("|"); diff --git a/src/core/internal/gc/bits.d b/src/core/internal/gc/bits.d index 11fb302a74..d50c38f0d2 100644 --- a/src/core/internal/gc/bits.d +++ b/src/core/internal/gc/bits.d @@ -7,6 +7,7 @@ */ module core.internal.gc.bits; +import core.internal.gc.os : os_mem_map, os_mem_unmap, HaveFork; import core.bitop; import core.stdc.string; @@ -32,19 +33,29 @@ struct GCBits wordtype* data; size_t nbits; - void Dtor() nothrow + void Dtor(bool share = false) nothrow { if (data) { - free(data); + static if (!HaveFork) + free(data); + else if (share) + os_mem_unmap(data, nwords * data[0].sizeof); + else + free(data); data = null; } } - void alloc(size_t nbits) nothrow + void alloc(size_t nbits, bool share = false) nothrow { this.nbits = nbits; - data = cast(typeof(data[0])*)calloc(nwords, data[0].sizeof); + static if (!HaveFork) + data = cast(typeof(data[0])*)calloc(nwords, data[0].sizeof); + else if (share) + data = cast(typeof(data[0])*)os_mem_map(nwords * data[0].sizeof, true); // Allocate as MAP_SHARED + else + data = cast(typeof(data[0])*)calloc(nwords, data[0].sizeof); if (!data) onOutOfMemoryError(); } @@ -431,6 +442,11 @@ struct GCBits memset(data, 0, nwords * wordtype.sizeof); } + void setAll() nothrow + { + memset(data, 0xFF, nwords * wordtype.sizeof); + } + void copy(GCBits *f) nothrow in { diff --git a/src/core/internal/gc/impl/conservative/gc.d b/src/core/internal/gc/impl/conservative/gc.d index c6c862b15c..0c49955c66 100644 --- a/src/core/internal/gc/impl/conservative/gc.d +++ b/src/core/internal/gc/impl/conservative/gc.d @@ -29,6 +29,8 @@ module core.internal.gc.impl.conservative.gc; /***************************************************/ version = COLLECT_PARALLEL; // parallel scanning +version (Posix) + version = COLLECT_FORK; import core.internal.gc.bits; import core.internal.gc.os; @@ -89,6 +91,10 @@ private // to avoid inlining - see issue 13725. void onInvalidMemoryOperationError(void* pretend_sideffect = null) @trusted pure nothrow @nogc; void onOutOfMemoryErrorNoGC() @trusted nothrow @nogc; + + version (COLLECT_FORK) + version (OSX) + pid_t __fork() nothrow; } enum @@ -1029,7 +1035,7 @@ class ConservativeGC : GC // when collecting. static size_t go(Gcx* gcx) nothrow { - return gcx.fullcollect(); + return gcx.fullcollect(false, true); // standard stop the world } immutable result = runLocked!go(gcx); @@ -1056,7 +1062,7 @@ class ConservativeGC : GC // when collecting. static size_t go(Gcx* gcx) nothrow { - return gcx.fullcollect(true); + return gcx.fullcollect(true, true, true); // standard stop the world } runLocked!go(gcx); } @@ -1253,6 +1259,12 @@ struct Gcx auto rangesLock = shared(AlignedSpinLock)(SpinLock.Contention.brief); Treap!Root roots; Treap!Range ranges; + bool minimizeAfterNextCollection = false; + version (COLLECT_FORK) + { + private pid_t markProcPid = 0; + bool shouldFork; + } debug(INVARIANT) bool initialized; debug(INVARIANT) bool inCollection; @@ -1303,6 +1315,9 @@ struct Gcx } } debug(INVARIANT) initialized = true; + version (COLLECT_FORK) + shouldFork = config.fork; + } void Dtor() @@ -1408,11 +1423,22 @@ struct Gcx auto pool = list.pool; auto biti = cast(size_t)(cast(void*)list - pool.baseAddr) >> Pool.ShiftBy.Small; assert(pool.freebits.test(biti)); + ppprev = pprev; + pprev = prev; + prev = list; } } } } + @property bool collectInProgress() const nothrow + { + version (COLLECT_FORK) + return markProcPid != 0; + else + return false; + } + /** * @@ -1687,7 +1713,7 @@ struct Gcx if (!newPool(1, false)) { // out of memory => try to free some memory - fullcollect(); + fullcollect(false, true); // stop the world if (lowMem) minimize(); recoverNextPage(bin); @@ -1710,8 +1736,11 @@ struct Gcx // Return next item from free list bucket[bin] = (cast(List*)p).next; auto pool = (cast(List*)p).pool; + auto biti = (p - pool.baseAddr) >> pool.shiftBy; assert(pool.freebits.test(biti)); + if (collectInProgress) + pool.mark.setLocked(biti); // be sure that the child is aware of the page being used pool.freebits.clear(biti); if (bits) pool.setBits(biti, bits); @@ -1774,14 +1803,14 @@ struct Gcx if (!tryAllocNewPool()) { // disabled but out of memory => try to free some memory - fullcollect(); - minimize(); + minimizeAfterNextCollection = true; + fullcollect(false, true); } } else if (usedLargePages > 0) { + minimizeAfterNextCollection = true; fullcollect(); - minimize(); } // If alloc didn't yet succeed retry now that we collected/minimized if (!pool && !tryAlloc() && !tryAllocNewPool()) @@ -1791,6 +1820,8 @@ struct Gcx assert(pool); debug(PRINTF) printFreeInfo(&pool.base); + if (collectInProgress) + pool.mark.setLocked(pn); usedLargePages += npages; debug(PRINTF) printFreeInfo(&pool.base); @@ -1859,6 +1890,8 @@ struct Gcx if (pool) { pool.initialize(npages, isLargeObject); + if (collectInProgress) + pool.mark.setAll(); if (!pool.baseAddr || !pooltable.insert(pool)) { pool.Dtor(); @@ -2005,7 +2038,7 @@ struct Gcx /** * Search a range of memory values and mark any pointers into the GC pool. */ - private void mark(bool precise, bool parallel)(ScanRange!precise rng) scope nothrow + private void mark(bool precise, bool parallel, bool shared_mem)(ScanRange!precise rng) scope nothrow { alias toscan = scanStack!precise; @@ -2089,7 +2122,7 @@ struct Gcx biti = offsetBase >> Pool.ShiftBy.Small; //debug(PRINTF) printf("\t\tbiti = x%x\n", biti); - if (!pool.mark.testAndSet!parallel(biti) && !pool.noscan.test(biti)) + if (!pool.mark.testAndSet!shared_mem(biti) && !pool.noscan.test(biti)) { tgt.pbot = pool.baseAddr + offsetBase; tgt.ptop = tgt.pbot + binsize[bin]; @@ -2116,7 +2149,7 @@ struct Gcx if (tgt.pbot != sentinel_sub(p) && pool.nointerior.nbits && pool.nointerior.test(biti)) goto LnextPtr; - if (!pool.mark.testAndSet!parallel(biti) && !pool.noscan.test(biti)) + if (!pool.mark.testAndSet!shared_mem(biti) && !pool.noscan.test(biti)) { tgt.ptop = tgt.pbot + (cast(LargeObjectPool*)pool).getSize(pn); goto LaddLargeRange; @@ -2131,7 +2164,7 @@ struct Gcx if (pool.nointerior.nbits && pool.nointerior.test(biti)) goto LnextPtr; - if (!pool.mark.testAndSet!parallel(biti) && !pool.noscan.test(biti)) + if (!pool.mark.testAndSet!shared_mem(biti) && !pool.noscan.test(biti)) { tgt.pbot = pool.baseAddr + (pn * PAGESIZE); tgt.ptop = tgt.pbot + (cast(LargeObjectPool*)pool).getSize(pn); @@ -2238,16 +2271,16 @@ struct Gcx } } - void markConservative(void *pbot, void *ptop) scope nothrow + void markConservative(bool shared_mem)(void *pbot, void *ptop) scope nothrow { if (pbot < ptop) - mark!(false, false)(ScanRange!false(pbot, ptop)); + mark!(false, false, shared_mem)(ScanRange!false(pbot, ptop)); } - void markPrecise(void *pbot, void *ptop) scope nothrow + void markPrecise(bool shared_mem)(void *pbot, void *ptop) scope nothrow { if (pbot < ptop) - mark!(true, false)(ScanRange!true(pbot, ptop, null)); + mark!(true, false, shared_mem)(ScanRange!true(pbot, ptop, null)); } version (COLLECT_PARALLEL) @@ -2608,11 +2641,145 @@ struct Gcx return recoverPool[bin] = poolIndex < npools ? cast(SmallObjectPool*)pool : null; } + version (COLLECT_FORK) + void disableFork() nothrow + { + markProcPid = 0; + shouldFork = false; + } + + version (COLLECT_FORK) + ChildStatus collectFork(bool block) nothrow + { + typeof(return) rc = wait_pid(markProcPid, block); + final switch (rc) + { + case ChildStatus.done: + debug(COLLECT_PRINTF) printf("\t\tmark proc DONE (block=%d)\n", + cast(int) block); + markProcPid = 0; + // process GC marks then sweep + thread_suspendAll(); + thread_processGCMarks(&isMarked); + thread_resumeAll(); + break; + case ChildStatus.running: + debug(COLLECT_PRINTF) printf("\t\tmark proc RUNNING\n"); + if (!block) + break; + // Something went wrong, if block is true, wait() should never + // return RUNNING. + goto case ChildStatus.error; + case ChildStatus.error: + debug(COLLECT_PRINTF) printf("\t\tmark proc ERROR\n"); + // Try to keep going without forking + // and do the marking in this thread + break; + } + return rc; + } + + version (COLLECT_FORK) + ChildStatus markFork(bool nostack, bool block, bool doParallel) nothrow + { + // Forking is enabled, so we fork() and start a new concurrent mark phase + // in the child. If the collection should not block, the parent process + // tells the caller no memory could be recycled immediately (if this collection + // was triggered by an allocation, the caller should allocate more memory + // to fulfill the request). + // If the collection should block, the parent will wait for the mark phase + // to finish before returning control to the mutator, + // but other threads are restarted and may run in parallel with the mark phase + // (unless they allocate or use the GC themselves, in which case + // the global GC lock will stop them). + // fork now and sweep later + int child_mark() scope + { + if (doParallel) + markParallel(nostack); + else if (ConservativeGC.isPrecise) + markAll!(markPrecise!true)(nostack); + else + markAll!(markConservative!true)(nostack); + return 0; + } + + import core.stdc.stdlib : _Exit; + debug (PRINTF_TO_FILE) + { + import core.stdc.stdio : fflush; + fflush(null); // avoid duplicated FILE* output + } + version (OSX) + { + auto pid = __fork(); // avoids calling handlers (from libc source code) + } + else version (linux) + { + // clone() fits better as we don't want to do anything but scanning in the child process. + // no fork-handlera are called, so we can avoid deadlocks due to malloc locks. Probably related: + // https://sourceware.org/bugzilla/show_bug.cgi?id=4737 + import core.sys.linux.sched : clone; + import core.sys.posix.signal : SIGCHLD; + enum CLONE_CHILD_CLEARTID = 0x00200000; /* Register exit futex and memory */ + const flags = CLONE_CHILD_CLEARTID | SIGCHLD; // child thread id not needed + scope int delegate() scope dg = &child_mark; + extern(C) static int wrap_delegate(void* arg) + { + auto dg = cast(int delegate() scope*)arg; + return (*dg)(); + } + char[256] stackbuf; // enough stack space for clone() to place some info for the child without stomping the parent stack + auto stack = stackbuf.ptr + (isStackGrowingDown ? stackbuf.length : 0); + auto pid = clone(&wrap_delegate, stack, flags, &dg); + } + else + { + fork_needs_lock = false; + auto pid = fork(); + fork_needs_lock = true; + } + assert(pid != -1); + switch (pid) + { + case -1: // fork() failed, retry without forking + return ChildStatus.error; + case 0: // child process (not run with clone) + child_mark(); + _Exit(0); + default: // the parent + thread_resumeAll(); + if (!block) + { + markProcPid = pid; + return ChildStatus.running; + } + ChildStatus r = wait_pid(pid); // block until marking is done + if (r == ChildStatus.error) + { + thread_suspendAll(); + // there was an error + // do the marking in this thread + disableFork(); + if (doParallel) + markParallel(nostack); + else if (ConservativeGC.isPrecise) + markAll!(markPrecise!false)(nostack); + else + markAll!(markConservative!false)(nostack); + } else { + assert(r == ChildStatus.done); + assert(r != ChildStatus.running); + } + } + return ChildStatus.done; // waited for the child + } /** * Return number of full pages free'd. + * The collection is done concurrently only if block and isFinal are false. */ - size_t fullcollect(bool nostack = false) nothrow + size_t fullcollect(bool nostack = false, bool block = false, bool isFinal = false) nothrow { // It is possible that `fullcollect` will be called from a thread which // is not yet registered in runtime (because allocating `new Thread` is @@ -2626,23 +2793,52 @@ struct Gcx begin = start = currTime; debug(COLLECT_PRINTF) printf("Gcx.fullcollect()\n"); + version (COLLECT_PARALLEL) + { + bool doParallel = config.parallel > 0 && !config.fork; + if (doParallel && !scanThreadData) + { + if (isFinal) // avoid starting threads for parallel marking + doParallel = false; + else + startScanThreads(); + } + } + else + enum doParallel = false; + //printf("\tpool address range = %p .. %p\n", minAddr, maxAddr); + version (COLLECT_FORK) + bool doFork = shouldFork; + else + enum doFork = false; + + if (doFork && collectInProgress) { - version (COLLECT_PARALLEL) + version (COLLECT_FORK) { - bool doParallel = config.parallel > 0; - if (doParallel && !scanThreadData) + // If there is a mark process running, check if it already finished. + // If that is the case, we move to the sweep phase. + // If it's still running, either we block until the mark phase is + // done (and then sweep to finish the collection), or in case of error + // we redo the mark phase without forking. + ChildStatus rc = collectFork(block); + final switch (rc) { - if (nostack) // only used during shutdown, avoid starting threads for parallel marking - doParallel = false; - else - startScanThreads(); + case ChildStatus.done: + break; + case ChildStatus.running: + return 0; + case ChildStatus.error: + disableFork(); + goto Lmark; } } - else - enum doParallel = false; - + } + else + { +Lmark: // lock roots and ranges around suspending threads b/c they're not reentrant safe rangesLock.lock(); rootsLock.lock(); @@ -2661,7 +2857,34 @@ struct Gcx prepTime += (stop - start); start = stop; - if (doParallel) + if (doFork && !isFinal && !block) // don't start a new fork during termination + { + version (COLLECT_FORK) + { + auto forkResult = markFork(nostack, block, doParallel); + final switch (forkResult) + { + case ChildStatus.error: + disableFork(); + goto Lmark; + case ChildStatus.running: + // update profiling informations + stop = currTime; + markTime += (stop - start); + Duration pause = stop - begin; + if (pause > maxPauseTime) + maxPauseTime = pause; + pauseTime += pause; + return 0; + case ChildStatus.done: + break; + } + // if we get here, forking failed and a standard STW collection got issued + // threads were suspended again, restart them + thread_suspendAll(); + } + } + else if (doParallel) { version (COLLECT_PARALLEL) markParallel(nostack); @@ -2669,15 +2892,20 @@ struct Gcx else { if (ConservativeGC.isPrecise) - markAll!markPrecise(nostack); + markAll!(markPrecise!false)(nostack); else - markAll!markConservative(nostack); + markAll!(markConservative!false)(nostack); } thread_processGCMarks(&isMarked); thread_resumeAll(); + isFinal = false; } + // If we get here with the forking GC, the child process has finished the marking phase + // or block == true and we are using standard stop the world collection. + // It is time to sweep + stop = currTime; markTime += (stop - start); Duration pause = stop - begin; @@ -2694,6 +2922,14 @@ struct Gcx ConservativeGC._inFinalizer = false; } + // minimize() should be called only after a call to fullcollect + // terminates with a sweep + if (minimizeAfterNextCollection || lowMem) + { + minimizeAfterNextCollection = false; + minimize(); + } + // init bucket lists bucket[] = null; foreach (Bins bin; 0..B_NUMSMALL) @@ -2709,7 +2945,8 @@ struct Gcx ++numCollections; updateCollectThresholds(); - + if (doFork && isFinal) + return fullcollect(true, true, false); return freedPages; } @@ -2761,22 +2998,26 @@ struct Gcx // Because that would leave the GC in an inconsistent state, // make sure no GC code is running by acquiring the lock here, // before a fork. + // This must not happen if fork is called from the GC with the lock already held + + __gshared bool fork_needs_lock = true; // racing condition with cocurrent calls of fork? + extern(C) static void _d_gcx_atfork_prepare() { - if (instance) + if (instance && fork_needs_lock) ConservativeGC.lockNR(); } extern(C) static void _d_gcx_atfork_parent() { - if (instance) + if (instance && fork_needs_lock) ConservativeGC.gcLock.unlock(); } extern(C) static void _d_gcx_atfork_child() { - if (instance) + if (instance && fork_needs_lock) { ConservativeGC.gcLock.unlock(); @@ -2859,9 +3100,9 @@ struct Gcx debug(PARALLEL_PRINTF) printf("mark %lld roots\n", cast(ulong)(ptop - pbot)); if (ConservativeGC.isPrecise) - mark!(true, true)(ScanRange!true(pbot, ptop, null)); + mark!(true, true, true)(ScanRange!true(pbot, ptop, null)); else - mark!(false, true)(ScanRange!false(pbot, ptop)); + mark!(false, true, true)(ScanRange!false(pbot, ptop)); busyThreads.atomicOp!"-="(1); @@ -3024,7 +3265,7 @@ struct Gcx { debug(PARALLEL_PRINTF) printf("scanBackground thread %d scanning range [%p,%lld] from stack\n", threadId, rng.pbot, cast(long) (rng.ptop - rng.pbot)); - mark!(precise, true)(rng); + mark!(precise, true, true)(rng); } busyThreads.atomicOp!"-="(1); } @@ -3118,7 +3359,10 @@ struct Pool topAddr = baseAddr + poolsize; auto nbits = cast(size_t)poolsize >> shiftBy; - mark.alloc(nbits); + version (COLLECT_FORK) + mark.alloc(nbits, config.fork); + else + mark.alloc(nbits); if (ConservativeGC.isPrecise) { if (isLargeObject) @@ -3207,7 +3451,7 @@ struct Pool bPageOffsets = null; } - mark.Dtor(); + mark.Dtor(config.fork); if (ConservativeGC.isPrecise) { if (isLargeObject) diff --git a/src/core/internal/gc/os.d b/src/core/internal/gc/os.d index b84308398a..ca4cbe2b1c 100644 --- a/src/core/internal/gc/os.d +++ b/src/core/internal/gc/os.d @@ -36,6 +36,51 @@ else version (Posix) import core.sys.posix.sys.mman; import core.stdc.stdlib; + + /// Possible results for the wait_pid() function. + enum ChildStatus + { + done, /// The process has finished successfully + running, /// The process is still running + error /// There was an error waiting for the process + } + + /** + * Wait for a process with PID pid to finish. + * + * If block is false, this function will not block, and return ChildStatus.running if + * the process is still running. Otherwise it will return always ChildStatus.done + * (unless there is an error, in which case ChildStatus.error is returned). + */ + ChildStatus wait_pid(pid_t pid, bool block = true) nothrow @nogc + { + import core.exception : onForkError; + + int status = void; + pid_t waited_pid = void; + // In the case where we are blocking, we need to consider signals + // arriving while we wait, and resume the waiting if EINTR is returned + do { + errno = 0; + waited_pid = waitpid(pid, &status, block ? 0 : WNOHANG); + } + while (waited_pid == -1 && errno == EINTR); + if (waited_pid == 0) + return ChildStatus.running; + else if (errno == ECHILD) + return ChildStatus.done; // someone called posix.syswait + else if (waited_pid != pid || status != 0) + { + onForkError(); + return ChildStatus.error; + } + return ChildStatus.done; + } + + public import core.sys.posix.unistd: pid_t, fork; + import core.sys.posix.sys.wait: waitpid, WNOHANG; + import core.stdc.errno: errno, EINTR, ECHILD; + //version = GC_Use_Alloc_MMap; } else @@ -59,10 +104,19 @@ else static assert(false, "No supported allocation methods available."); static if (is(typeof(VirtualAlloc))) // version (GC_Use_Alloc_Win32) { + /** + * Indicates if an implementation supports fork(). + * + * The value shown here is just demostrative, the real value is defined based + * on the OS it's being compiled in. + * enum HaveFork = true; + */ + enum HaveFork = false; + /** * Map memory. */ - void *os_mem_map(size_t nbytes) nothrow + void *os_mem_map(size_t nbytes) nothrow @nogc { return VirtualAlloc(null, nbytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); @@ -75,35 +129,40 @@ static if (is(typeof(VirtualAlloc))) // version (GC_Use_Alloc_Win32) * 0 success * !=0 failure */ - int os_mem_unmap(void *base, size_t nbytes) nothrow + int os_mem_unmap(void *base, size_t nbytes) nothrow @nogc { return cast(int)(VirtualFree(base, 0, MEM_RELEASE) == 0); } } else static if (is(typeof(mmap))) // else version (GC_Use_Alloc_MMap) { - void *os_mem_map(size_t nbytes) nothrow + enum HaveFork = true; + + void *os_mem_map(size_t nbytes, bool share = false) nothrow @nogc { void *p; - p = mmap(null, nbytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + auto map_f = share ? MAP_SHARED : MAP_PRIVATE; + p = mmap(null, nbytes, PROT_READ | PROT_WRITE, map_f | MAP_ANON, -1, 0); return (p == MAP_FAILED) ? null : p; } - int os_mem_unmap(void *base, size_t nbytes) nothrow + int os_mem_unmap(void *base, size_t nbytes) nothrow @nogc { return munmap(base, nbytes); } } else static if (is(typeof(valloc))) // else version (GC_Use_Alloc_Valloc) { - void *os_mem_map(size_t nbytes) nothrow + enum HaveFork = false; + + void *os_mem_map(size_t nbytes) nothrow @nogc { return valloc(nbytes); } - int os_mem_unmap(void *base, size_t nbytes) nothrow + int os_mem_unmap(void *base, size_t nbytes) nothrow @nogc { free(base); return 0; @@ -116,6 +175,7 @@ else static if (is(typeof(malloc))) // else version (GC_Use_Alloc_Malloc) // to PAGESIZE alignment, there will be space for a void* at the end // after PAGESIZE bytes used by the GC. + enum HaveFork = false; import core.internal.gc.impl.conservative.gc; @@ -123,7 +183,7 @@ else static if (is(typeof(malloc))) // else version (GC_Use_Alloc_Malloc) const size_t PAGE_MASK = PAGESIZE - 1; - void *os_mem_map(size_t nbytes) nothrow + void *os_mem_map(size_t nbytes) nothrow @nogc { byte *p, q; p = cast(byte *) malloc(nbytes + PAGESIZE); if (!p) @@ -134,7 +194,7 @@ else static if (is(typeof(malloc))) // else version (GC_Use_Alloc_Malloc) } - int os_mem_unmap(void *base, size_t nbytes) nothrow + int os_mem_unmap(void *base, size_t nbytes) nothrow @nogc { free( *cast(void**)( cast(byte*) base + nbytes ) ); return 0; diff --git a/test/gc/Makefile b/test/gc/Makefile index 9a1af125c8..7ee6a13d1a 100644 --- a/test/gc/Makefile +++ b/test/gc/Makefile @@ -2,7 +2,7 @@ include ../common.mak TESTS := attributes sentinel sentinel1 sentinel2 printf memstomp invariant logging \ precise precisegc forkgc forkgc2 \ - sigmaskgc startbackgc recoverfree nocollect + sigmaskgc startbackgc recoverfree nocollect concurrent precise_concurrent hospital SRC_GC = ../../src/core/internal/gc/impl/conservative/gc.d SRC = $(SRC_GC) ../../src/rt/lifetime.d @@ -45,6 +45,14 @@ $(ROOT)/precise.done: RUN_ARGS += --DRT-gcopt=gc:precise $(ROOT)/precisegc: $(SRC) precisegc.d $(DMD) $(UDFLAGS) -gx -of$@ $(SRC) precisegc.d +$(ROOT)/concurrent: $(SRC) + $(DMD) $(UDFLAGS) -main -of$@ $(SRC) +$(ROOT)/concurrent.done: RUN_ARGS += --DRT-gcopt=fork:1 + +$(ROOT)/precise_concurrent: $(SRC) + $(DMD) $(UDFLAGS) -main -of$@ $(SRC) +$(ROOT)/precise_concurrent.done: RUN_ARGS += "--DRT-gcopt=gc:precise fork:1" + $(ROOT)/attributes: attributes.d $(DMD) $(UDFLAGS) -of$@ attributes.d @@ -66,5 +74,9 @@ $(ROOT)/recoverfree: recoverfree.d $(ROOT)/nocollect: nocollect.d $(DMD) $(DFLAGS) -of$@ nocollect.d +$(ROOT)/hospital: hospital.d + $(DMD) $(DFLAGS) -d -of$@ hospital.d +$(ROOT)/hospital.done: RUN_ARGS += --DRT-gcopt=fork:1 + clean: rm -rf $(ROOT) diff --git a/test/gc/hospital.d b/test/gc/hospital.d new file mode 100644 index 0000000000..fab4ebeec1 --- /dev/null +++ b/test/gc/hospital.d @@ -0,0 +1,252 @@ +// NOTE: the shootout appears to be BSD licensed content. +// Including this in the test suite based on that license. + +/* The Great Computer Language Shootout + http://shootout.alioth.debian.org/ + + Unoptimised reference implementation + + contributed by Isaac Gouy +*/ + +import core.memory; + +int main(string[] args) +{ + int n = 1000; + + HealthcareRegion healthcareSystem = HealthcareRegion.Create(); + + for(int i = 0; i < n; i++) + healthcareSystem.TransferPatients(); + + Totals t = healthcareSystem.AccumulateTotals(); + + version (VERBOSE) + { + import core.stdc.stdio; + printf("Patients: %lld\n", t.patients); + printf("Time: %lld\n", t.hospitalTime); + printf("Visits: %lld\n", t.hospitalVisits); + } + + if (n == 1000) + { + assert(t.patients == 102515); + assert(t.hospitalTime == 33730654); + assert(t.hospitalVisits == 106371); + } + return 0; +} + +class HealthcareRegion +{ +public: + static HealthcareRegion Create() + { + return HealthcareRegion.Create(LEVELS, 0, 42); + } + + static HealthcareRegion Create(int level, int seed1, int seed2) + { + HealthcareRegion r = null; + + if(level > 0) + { + r = new HealthcareRegion(level, seed1*seed2); + for(ptrdiff_t i = r.districts.length-1; i >= 0; i--) + r.districts[i] = Create(level-1, cast(int)((seed1*4)+i+1), seed2); + } + return r; + } + + this(int level, int s) + { + districts = new HealthcareRegion[DISTRICTS]; + localHospital = new Hospital(level == LEVELS, level, s); + } + +private: + enum int LEVELS = 5, DISTRICTS = 4; + HealthcareRegion[] districts; + Hospital localHospital; + +package: + Patient[] TransferPatients() + { + for(ptrdiff_t i = districts.length-1; i >= 0; i--) + if(districts[i]) + foreach(Patient p; districts[i].TransferPatients().dup) + localHospital.NewArrival(p); + + localHospital.TriageExaminationTreatment(); + + return localHospital.RegionalTransferPatients(); + } + + Totals AccumulateTotals() + { + Totals t = new Totals(); + for(ptrdiff_t i = districts.length-1; i >= 0; i--) + if(districts[i]) + t += districts[i].AccumulateTotals(); + + localHospital.AccumulateTotals(t); + + return t; + } +} + +class Hospital +{ + public this(bool hasNoRegionalHospital, int level, int seed) + { + this.hasNoRegionalHospital = hasNoRegionalHospital; + availableStaff = 1 << (level - 1); + discharged = new Totals(); + this.seed = seed; + } + +package: + void TriageExaminationTreatment() + { + DischargePatients(); + TreatOrTransferPatients(); + TriagePatients(); + + if(genRandom(1.0) > 0.7) + { Patient p = new Patient(); + NewArrival(p); + } + } + + Patient[] RegionalTransferPatients() + { + return transfers; + } + + void AccumulateTotals(Totals t) + { + foreach(Patient p; triage) t.Plus(p); + foreach(Patient p; examination) t.Plus(p); + foreach(Patient p; treatment) t.Plus(p); + t += discharged; + } + + void NewArrival(Patient p) + { + p.hospitalVisits++; + if(availableStaff > 0) + { + availableStaff--; + examination ~= p; + p.remainingTime = 3; + p.hospitalTime += 3; + } else { + triage ~= p; + } + } + +private: + Patient[] triage, examination, treatment, transfers; + Totals discharged; + int availableStaff; + bool hasNoRegionalHospital; + + void DischargePatients() + { + for(ptrdiff_t i = treatment.length-1; i >= 0; i--) + { + Patient p = treatment[i]; + p.remainingTime -= 1; + if(!p.remainingTime) + { + availableStaff++; + treatment = treatment[0..i] ~ treatment[i+1..$]; + discharged.Plus(p); + } + } + } + + void TreatOrTransferPatients() + { + delete transfers; + + for(ptrdiff_t i = examination.length-1; i >= 0; i--) + { + Patient p = examination[i]; + p.remainingTime -= 1; + + if(!p.remainingTime) + { + // no transfer + if(genRandom(1.0) > 0.1 || hasNoRegionalHospital) + { + examination = examination[0..i] ~ examination[i+1..$]; + treatment ~= p; + p.remainingTime = 10; + p.hospitalTime += 10; + } else { + // transfer + availableStaff++; + examination = examination[0..i] ~ examination[i+1..$]; + transfers ~= p; + } + } + } + } + + void TriagePatients() + { + for(ptrdiff_t i = triage.length-1; i >= 0; i--) + { + Patient p = triage[i]; + assert(p.hospitalTime != 0x7fff); + if(availableStaff > 0) + { + availableStaff--; + p.remainingTime = 3; + p.hospitalTime += 3; + triage = triage[0..i] ~ triage[i+1..$]; + examination ~= p; + } else { + p.hospitalTime++; + } + } + } + + int seed = 42; + const int IM = 139968; + const int IA = 3877; + const int IC = 29573; + double genRandom(double max) + { + return(max * (seed = (seed * IA + IC) % IM) / IM); + } +} + +class Patient +{ + package int remainingTime, hospitalTime, hospitalVisits; +} + +class Totals +{ + public Totals opOpAssign(string op)(Totals b) if (op == "+") + { + patients += b.patients; + hospitalTime += b.hospitalTime; + hospitalVisits += b.hospitalVisits; + return this; + } + +package: + long patients, hospitalTime, hospitalVisits; + + void Plus(Patient p) + { + patients++; + hospitalTime += p.hospitalTime; + hospitalVisits += p.hospitalVisits; + } +}