Two step allocator for 64-bit systems
authorGiovanni Campagna <gcampagn@cs.stanford.edu>
Fri, 17 Jul 2015 10:55:49 +0000 (11:55 +0100)
committerSimon Marlow <marlowsd@gmail.com>
Wed, 22 Jul 2015 16:50:02 +0000 (17:50 +0100)
Summary:
The current OS memory allocator conflates the concepts of allocating
address space and allocating memory, which makes the HEAP_ALLOCED()
implementation excessively complicated (as the only thing it cares
about is address space layout) and slow. Instead, what we want
is to allocate a single insanely large contiguous block of address
space (to make HEAP_ALLOCED() checks fast), and then commit subportions
of that in 1MB blocks as we did before.
This is currently behind a flag, USE_LARGE_ADDRESS_SPACE, that is only enabled for
certain OSes.

Test Plan: validate

Reviewers: simonmar, ezyang, austin

Subscribers: thomie, carter

Differential Revision: https://phabricator.haskell.org/D524

GHC Trac Issues: #9706

15 files changed:
configure.ac
includes/rts/storage/MBlock.h
rts/Sparks.c
rts/posix/OSMem.c
rts/sm/BlockAlloc.c
rts/sm/GC.h
rts/sm/HeapAlloc.h [new file with mode: 0644]
rts/sm/MBlock.c
rts/sm/OSMem.h
rts/win32/OSMem.c
testsuite/tests/rts/Makefile
testsuite/tests/rts/all.T
testsuite/tests/rts/outofmem.stderr-ws-64
testsuite/tests/rts/outofmem.stdout
testsuite/tests/rts/testmblockalloc.c [new file with mode: 0644]

index 8d66f3f..d896c8b 100644 (file)
@@ -968,6 +968,42 @@ else
 fi
 AC_SUBST(HavePapi)
 
+dnl large address space support (see includes/rts/storage/MBlock.h)
+dnl
+dnl Darwin has vm_allocate/vm_protect
+dnl Linux has mmap(MAP_NORESERVE)/madv(MADV_DONTNEED)
+dnl FreeBSD, Solaris and maybe other have MAP_NORESERVE/MADV_FREE
+dnl (They also have MADV_DONTNEED, but it means something else!)
+dnl
+dnl Windows has VirtualAlloc MEM_RESERVE/MEM_COMMIT, however
+dnl it counts page-table space as committed memory, and so quickly
+dnl runs out of paging file when we have multiple processes reserving
+dnl 1TB of address space, we get the following error:
+dnl    VirtualAlloc MEM_RESERVE 1099512676352 bytes failed: The paging file is too small for this operation to complete.
+dnl
+use_large_address_space=no
+if test "$ac_cv_sizeof_void_p" -eq 8 ; then
+   if test "$ghc_host_os" = "darwin" ; then
+           use_large_address_space=yes
+   else
+      AC_CHECK_DECLS([MAP_NORESERVE, MADV_FREE, MADV_DONTNEED],[],[],
+[
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+])
+      if test "$ac_cv_have_decl_MAP_NORESERVE" = "yes" &&
+         test "$ac_cv_have_decl_MADV_FREE" = "yes" ||
+         test "$ac_cv_have_decl_MADV_DONTNEED" = "yes" ; then
+              use_large_address_space=yes
+      fi
+   fi
+fi
+if test "$use_large_address_space" = "yes" ; then
+   AC_DEFINE([USE_LARGE_ADDRESS_SPACE], [1], [Enable single heap address space support])
+fi
+
 if test "$HAVE_DOCBOOK_XSL" = "NO" ||
    test "$XsltprocCmd" = ""
 then
index 29105ca..046990e 100644 (file)
@@ -19,203 +19,15 @@ extern void initMBlocks(void);
 extern void * getMBlock(void);
 extern void * getMBlocks(nat n);
 extern void freeMBlocks(void *addr, nat n);
+extern void releaseFreeMemory(void);
 extern void freeAllMBlocks(void);
 
-extern void *getFirstMBlock(void);
-extern void *getNextMBlock(void *mblock);
+extern void *getFirstMBlock(void **state);
+extern void *getNextMBlock(void **state, void *mblock);
 
 #ifdef THREADED_RTS
 // needed for HEAP_ALLOCED below
 extern SpinLock gc_alloc_block_sync;
 #endif
 
-/* -----------------------------------------------------------------------------
-   The HEAP_ALLOCED() test.
-
-   HEAP_ALLOCED is called FOR EVERY SINGLE CLOSURE during GC.
-   It needs to be FAST.
-
-   See wiki commentary at
-     http://ghc.haskell.org/trac/ghc/wiki/Commentary/HeapAlloced
-
-   Implementation of HEAP_ALLOCED
-   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-   Since heap is allocated in chunks of megablocks (MBLOCK_SIZE), we
-   can just use a table to record which megablocks in the address
-   space belong to the heap.  On a 32-bit machine, with 1Mb
-   megablocks, using 8 bits for each entry in the table, the table
-   requires 4k.  Lookups during GC will be fast, because the table
-   will be quickly cached (indeed, performance measurements showed no
-   measurable difference between doing the table lookup and using a
-   constant comparison).
-
-   On 64-bit machines, we cache one 12-bit block map that describes
-   4096 megablocks or 4GB of memory. If HEAP_ALLOCED is called for
-   an address that is not in the cache, it calls slowIsHeapAlloced
-   (see MBlock.c) which will find the block map for the 4GB block in
-   question.
-   -------------------------------------------------------------------------- */
-
-#if SIZEOF_VOID_P == 4
-extern StgWord8 mblock_map[];
-
-/* On a 32-bit machine a 4KB table is always sufficient */
-# define MBLOCK_MAP_SIZE        4096
-# define MBLOCK_MAP_ENTRY(p)    ((StgWord)(p) >> MBLOCK_SHIFT)
-# define HEAP_ALLOCED(p)        mblock_map[MBLOCK_MAP_ENTRY(p)]
-# define HEAP_ALLOCED_GC(p)     HEAP_ALLOCED(p)
-
-/* -----------------------------------------------------------------------------
-   HEAP_ALLOCED for 64-bit machines.
-
- Here are some cache layout options:
-
- [1]
- 16KB cache of 16-bit entries, 1MB lines (capacity 8GB)
-  mblock size =          20 bits
-  entries   =     8192   13 bits
-  line size =             0 bits (1 bit of value)
-  tag size  =            15 bits
-                       = 48 bits
-
- [2]
- 32KB cache of 16-bit entries, 4MB lines (capacity 32GB)
-  mblock size =          20 bits
-  entries   =    16384   14 bits
-  line size =             2 bits (4 bits of value)
-  tag size  =            12 bits
-                       = 48 bits
-
- [3]
- 16KB cache of 16-bit entries, 2MB lines (capacity 16GB)
-  mblock size =          20 bits
-  entries   =    8192    13 bits
-  line size =             1 bits (2 bits of value)
-  tag size  =            14 bits
-                       = 48 bits
-
- [4]
- 4KB cache of 32-bit entries, 16MB lines (capacity 16GB)
-  mblock size =          20 bits
-  entries   =     1024   10 bits
-  line size =             4 bits (16 bits of value)
-  tag size  =            14 bits
-                       = 48 bits
-
- [5]
- 4KB cache of 64-bit entries, 32MB lines (capacity 16GB)
-  mblock size =          20 bits
-  entries   =     512     9 bits
-  line size =             5 bits (32 bits of value)
-  tag size  =            14 bits
-                       = 48 bits
-
- We actually use none of the above.  After much experimentation it was
- found that optimising the lookup is the most important factor,
- followed by reducing the number of misses.  To that end, we use a
- variant of [1] in which each cache entry is ((mblock << 1) + value)
- where value is 0 for non-heap and 1 for heap.  The cache entries can
- be 32 bits, since the mblock number is 48-20 = 28 bits, and we need
- 1 bit for the value.  The cache can be as big as we like, but
- currently we use 8k entries, giving us 8GB capacity.
-
- ---------------------------------------------------------------------------- */
-
-#elif SIZEOF_VOID_P == 8
-
-#define MBC_LINE_BITS 0
-#define MBC_TAG_BITS 15
-
-#if x86_64_HOST_ARCH
-// 32bits are enough for 'entry' as modern amd64 boxes have
-// only 48bit sized virtual addres.
-typedef StgWord32 MbcCacheLine;
-#else
-// 32bits is not enough here as some arches (like ia64) use
-// upper address bits to distinct memory areas.
-typedef StgWord64 MbcCacheLine;
-#endif
-
-typedef StgWord8  MBlockMapLine;
-
-#define MBLOCK_MAP_LINE(p)  (((StgWord)p & 0xffffffff) >> (MBLOCK_SHIFT + MBC_LINE_BITS))
-
-#define MBC_LINE_SIZE  (1<<MBC_LINE_BITS)
-#define MBC_SHIFT      (48 - MBLOCK_SHIFT - MBC_LINE_BITS - MBC_TAG_BITS)
-#define MBC_ENTRIES    (1<<MBC_SHIFT)
-
-extern MbcCacheLine mblock_cache[];
-
-#define MBC_LINE(p) ((StgWord)p >> (MBLOCK_SHIFT + MBC_LINE_BITS))
-
-#define MBLOCK_MAP_ENTRIES  (1 << (32 - MBLOCK_SHIFT - MBC_LINE_BITS))
-
-typedef struct {
-    StgWord32    addrHigh32;
-    MBlockMapLine lines[MBLOCK_MAP_ENTRIES];
-} MBlockMap;
-
-extern W_ mpc_misses;
-
-StgBool HEAP_ALLOCED_miss(StgWord mblock, void *p);
-
-INLINE_HEADER
-StgBool HEAP_ALLOCED(void *p)
-{
-    StgWord mblock;
-    nat entry_no;
-    MbcCacheLine entry, value;
-
-    mblock   = (StgWord)p >> MBLOCK_SHIFT;
-    entry_no = mblock & (MBC_ENTRIES-1);
-    entry    = mblock_cache[entry_no];
-    value    = entry ^ (mblock << 1);
-    // this formulation coaxes gcc into prioritising the value==1
-    // case, which we expect to be the most common.
-    // __builtin_expect() didn't have any useful effect (gcc-4.3.0).
-    if (value == 1) {
-        return 1;
-    } else if (value == 0) {
-        return 0;
-    } else {
-        // putting the rest out of line turned out to be a slight
-        // performance improvement:
-        return HEAP_ALLOCED_miss(mblock,p);
-    }
-}
-
-// In the parallel GC, the cache itself is safe to *read*, and can be
-// updated atomically, but we need to place a lock around operations
-// that touch the MBlock map.
-INLINE_HEADER
-StgBool HEAP_ALLOCED_GC(void *p)
-{
-    StgWord mblock;
-    nat entry_no;
-    MbcCacheLine entry, value;
-    StgBool b;
-
-    mblock   = (StgWord)p >> MBLOCK_SHIFT;
-    entry_no = mblock & (MBC_ENTRIES-1);
-    entry    = mblock_cache[entry_no];
-    value    = entry ^ (mblock << 1);
-    if (value == 1) {
-        return 1;
-    } else if (value == 0) {
-        return 0;
-    } else {
-        // putting the rest out of line turned out to be a slight
-        // performance improvement:
-        ACQUIRE_SPIN_LOCK(&gc_alloc_block_sync);
-        b = HEAP_ALLOCED_miss(mblock,p);
-        RELEASE_SPIN_LOCK(&gc_alloc_block_sync);
-        return b;
-    }
-}
-
-#else
-# error HEAP_ALLOCED not defined
-#endif
-
 #endif /* RTS_STORAGE_MBLOCK_H */
index ada2adf..ec07580 100644 (file)
@@ -14,6 +14,7 @@
 #include "Trace.h"
 #include "Prelude.h"
 #include "Sparks.h"
+#include "sm/HeapAlloc.h"
 
 #if defined(THREADED_RTS)
 
index 359df70..125ae10 100644 (file)
@@ -13,6 +13,7 @@
 
 #include "RtsUtils.h"
 #include "sm/OSMem.h"
+#include "sm/HeapAlloc.h"
 
 #ifdef HAVE_UNISTD_H
 #include <unistd.h>
@@ -72,23 +73,67 @@ void osMemInit(void)
 
    -------------------------------------------------------------------------- */
 
-// A wrapper around mmap(), to abstract away from OS differences in
-// the mmap() interface.
+/*
+ A wrapper around mmap(), to abstract away from OS differences in
+ the mmap() interface.
+
+ It supports the following operations:
+ - reserve: find a new chunk of available address space, and make it so
+            that we own it (no other library will get it), but don't actually
+            allocate memory for it
+            the addr is a hint for where to place the memory (and most
+            of the time the OS happily ignores!)
+ - commit: given a chunk of address space that we know we own, make sure
+           there is some memory backing it
+           the addr is not a hint, it must point into previously reserved
+           address space, or bad things happen
+ - reserve&commit: do both at the same time
+
+ The naming is chosen from the Win32 API (VirtualAlloc) which does the
+ same thing and has done so forever, while support for this in Unix systems
+ has only been added recently and is hidden in the posix portability mess.
+ It is confusing because to get the reserve behavior we need MAP_NORESERVE
+ (which tells the kernel not to allocate backing space), but heh...
+*/
+enum
+{
+    MEM_RESERVE = 1,
+    MEM_COMMIT = 2,
+    MEM_RESERVE_AND_COMMIT = MEM_RESERVE | MEM_COMMIT
+};
 
 static void *
-my_mmap (void *addr, W_ size)
+my_mmap (void *addr, W_ size, int operation)
 {
     void *ret;
+    int prot, flags;
+
+    if (operation & MEM_COMMIT)
+        prot = PROT_READ | PROT_WRITE;
+    else
+        prot = PROT_NONE;
+    if (operation == MEM_RESERVE)
+        flags = MAP_NORESERVE;
+    else if (operation == MEM_COMMIT)
+        flags = MAP_FIXED;
+    else
+        flags = 0;
 
 #if defined(solaris2_HOST_OS) || defined(irix_HOST_OS)
     {
-        int fd = open("/dev/zero",O_RDONLY);
-        ret = mmap(addr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
-        close(fd);
+        if (operation & MEM_RESERVE)
+        {
+            int fd = open("/dev/zero",O_RDONLY);
+            ret = mmap(addr, size, prot, flags | MAP_PRIVATE, fd, 0);
+            close(fd);
+        }
+        else
+        {
+            ret = mmap(addr, size, prot, flags | MAP_PRIVATE, -1, 0);
+        }
     }
 #elif hpux_HOST_OS
-    ret = mmap(addr, size, PROT_READ | PROT_WRITE,
-               MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+    ret = mmap(addr, size, prot, flags | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
 #elif darwin_HOST_OS
     // Without MAP_FIXED, Apple's mmap ignores addr.
     // With MAP_FIXED, it overwrites already mapped regions, whic
@@ -100,10 +145,16 @@ my_mmap (void *addr, W_ size)
 
     kern_return_t err = 0;
     ret = addr;
-    if(addr)    // try to allocate at address
-        err = vm_allocate(mach_task_self(),(vm_address_t*) &ret, size, FALSE);
-    if(!addr || err)    // try to allocate anywhere
-        err = vm_allocate(mach_task_self(),(vm_address_t*) &ret, size, TRUE);
+
+    if(operation & MEM_RESERVE)
+    {
+        if(addr)    // try to allocate at address
+            err = vm_allocate(mach_task_self(),(vm_address_t*) &ret,
+                              size, FALSE);
+        if(!addr || err)    // try to allocate anywhere
+            err = vm_allocate(mach_task_self(),(vm_address_t*) &ret,
+                              size, TRUE);
+    }
 
     if(err) {
         // don't know what the error codes mean exactly, assume it's
@@ -111,23 +162,24 @@ my_mmap (void *addr, W_ size)
         errorBelch("memory allocation failed (requested %" FMT_Word " bytes)",
                    size);
         stg_exit(EXIT_FAILURE);
-    } else {
+    }
+
+    if(operation & MEM_COMMIT) {
         vm_protect(mach_task_self(), (vm_address_t)ret, size, FALSE,
                    VM_PROT_READ|VM_PROT_WRITE);
     }
+
 #elif linux_HOST_OS
-    ret = mmap(addr, size, PROT_READ | PROT_WRITE,
-               MAP_ANON | MAP_PRIVATE, -1, 0);
+    ret = mmap(addr, size, prot, flags | MAP_ANON | MAP_PRIVATE, -1, 0);
     if (ret == (void *)-1 && errno == EPERM) {
         // Linux may return EPERM if it tried to give us
         // a chunk of address space below mmap_min_addr,
         // See Trac #7500.
-        if (addr != 0) {
+        if (addr != 0 && (operation & MEM_RESERVE)) {
             // Try again with no hint address.
             // It's not clear that this can ever actually help,
             // but since our alternative is to abort, we may as well try.
-            ret = mmap(0, size, PROT_READ | PROT_WRITE,
-                       MAP_ANON | MAP_PRIVATE, -1, 0);
+            ret = mmap(0, size, prot, flags | MAP_ANON | MAP_PRIVATE, -1, 0);
         }
         if (ret == (void *)-1 && errno == EPERM) {
             // Linux is not willing to give us any mapping,
@@ -137,8 +189,7 @@ my_mmap (void *addr, W_ size)
         }
     }
 #else
-    ret = mmap(addr, size, PROT_READ | PROT_WRITE,
-               MAP_ANON | MAP_PRIVATE, -1, 0);
+    ret = mmap(addr, size, prot, flags | MAP_ANON | MAP_PRIVATE, -1, 0);
 #endif
 
     if (ret == (void *)-1) {
@@ -168,7 +219,7 @@ gen_map_mblocks (W_ size)
     // Try to map a larger block, and take the aligned portion from
     // it (unmap the rest).
     size += MBLOCK_SIZE;
-    ret = my_mmap(0, size);
+    ret = my_mmap(0, size, MEM_RESERVE_AND_COMMIT);
 
     // unmap the slop bits around the chunk we allocated
     slop = (W_)ret & MBLOCK_MASK;
@@ -207,7 +258,7 @@ osGetMBlocks(nat n)
       // use gen_map_mblocks the first time.
       ret = gen_map_mblocks(size);
   } else {
-      ret = my_mmap(next_request, size);
+      ret = my_mmap(next_request, size, MEM_RESERVE_AND_COMMIT);
 
       if (((W_)ret & MBLOCK_MASK) != 0) {
           // misaligned block!
@@ -244,10 +295,11 @@ void osReleaseFreeMemory(void) {
 void osFreeAllMBlocks(void)
 {
     void *mblock;
+    void *state;
 
-    for (mblock = getFirstMBlock();
+    for (mblock = getFirstMBlock(&state);
          mblock != NULL;
-         mblock = getNextMBlock(mblock)) {
+         mblock = getNextMBlock(&state, mblock)) {
         munmap(mblock, MBLOCK_SIZE);
     }
 }
@@ -318,3 +370,103 @@ void setExecutable (void *p, W_ len, rtsBool exec)
         barf("setExecutable: failed to protect 0x%p\n", p);
     }
 }
+
+#ifdef USE_LARGE_ADDRESS_SPACE
+
+static void *
+osTryReserveHeapMemory (void *hint)
+{
+    void *base, *top;
+    void *start, *end;
+
+    /* We try to allocate MBLOCK_SPACE_SIZE + MBLOCK_SIZE,
+       because we need memory which is MBLOCK_SIZE aligned,
+       and then we discard what we don't need */
+
+    base = my_mmap(hint, MBLOCK_SPACE_SIZE + MBLOCK_SIZE, MEM_RESERVE);
+    top = (void*)((W_)base + MBLOCK_SPACE_SIZE + MBLOCK_SIZE);
+
+    if (((W_)base & MBLOCK_MASK) != 0) {
+        start = MBLOCK_ROUND_UP(base);
+        end = MBLOCK_ROUND_DOWN(top);
+        ASSERT(((W_)end - (W_)start) == MBLOCK_SPACE_SIZE);
+
+        if (munmap(base, (W_)start-(W_)base) < 0) {
+            sysErrorBelch("unable to release slop before heap");
+        }
+        if (munmap(end, (W_)top-(W_)end) < 0) {
+            sysErrorBelch("unable to release slop after heap");
+        }
+    } else {
+        start = base;
+    }
+
+    return start;
+}
+
+void *osReserveHeapMemory(void)
+{
+    int attempt;
+    void *at;
+
+    /* We want to ensure the heap starts at least 8 GB inside the address space,
+       to make sure that any dynamically loaded code will be close enough to the
+       original code so that short relocations will work. This is in particular
+       important on Darwin/Mach-O, because object files not compiled as shared
+       libraries are position independent but cannot be loaded about 4GB.
+
+       We do so with a hint to the mmap, and we verify the OS satisfied our
+       hint. We loop a few times in case there is already something allocated
+       there, but we bail if we cannot allocate at all.
+    */
+
+    attempt = 0;
+    do {
+        at = osTryReserveHeapMemory((void*)((W_)8 * (1 << 30) +
+                                            attempt * BLOCK_SIZE));
+    } while ((W_)at < ((W_)8 * (1 << 30)));
+
+    return at;
+}
+
+void osCommitMemory(void *at, W_ size)
+{
+    my_mmap(at, size, MEM_COMMIT);
+}
+
+void osDecommitMemory(void *at, W_ size)
+{
+    int r;
+
+    // First make the memory unaccessible (so that we get a segfault
+    // at the next attempt to touch it)
+    // We only do this in DEBUG because it forces the OS to remove
+    // all MMU entries for this page range, and there is no reason
+    // to do so unless there is memory pressure
+#ifdef DEBUG
+    r = mprotect(at, size, PROT_NONE);
+    if(r < 0)
+        sysErrorBelch("unable to make released memory unaccessible");
+#endif
+
+#ifdef MADV_FREE
+    // Try MADV_FREE first, FreeBSD has both and MADV_DONTNEED
+    // just swaps memory out
+    r = madvise(at, size, MADV_FREE);
+#else
+    r = madvise(at, size, MADV_DONTNEED);
+#endif
+    if(r < 0)
+        sysErrorBelch("unable to decommit memory");
+}
+
+void osReleaseHeapMemory(void)
+{
+    int r;
+
+    r = munmap((void*)mblock_address_space_begin, MBLOCK_SPACE_SIZE);
+    if(r < 0)
+        sysErrorBelch("unable to release address space");
+}
+
+#endif
index c2a5913..e721fb1 100644 (file)
@@ -736,7 +736,14 @@ void returnMemoryToOS(nat n /* megablocks */)
     }
     free_mblock_list = bd;
 
-    osReleaseFreeMemory();
+    // Ask the OS to release any address space portion
+    // that was associated with the just released MBlocks
+    //
+    // Historically, we used to ask the OS directly (via
+    // osReleaseFreeMemory()) - now the MBlock layer might
+    // have a reason to preserve the address space range,
+    // so we keep it
+    releaseFreeMemory();
 
     IF_DEBUG(gc,
         if (n != 0) {
@@ -869,11 +876,12 @@ void
 reportUnmarkedBlocks (void)
 {
     void *mblock;
+    void *state;
     bdescr *bd;
 
     debugBelch("Unreachable blocks:\n");
-    for (mblock = getFirstMBlock(); mblock != NULL;
-         mblock = getNextMBlock(mblock)) {
+    for (mblock = getFirstMBlock(&state); mblock != NULL;
+         mblock = getNextMBlock(&state, mblock)) {
         for (bd = FIRST_BDESCR(mblock); bd <= LAST_BDESCR(mblock); ) {
             if (!(bd->flags & BF_KNOWN) && bd->free != (P_)-1) {
                 debugBelch("  %p\n",bd);
index 571aa07..5744eb9 100644 (file)
@@ -16,6 +16,8 @@
 
 #include "BeginPrivate.h"
 
+#include "HeapAlloc.h"
+
 void GarbageCollect (rtsBool force_major_gc,
                      rtsBool do_heap_census,
                      nat gc_type, Capability *cap);
diff --git a/rts/sm/HeapAlloc.h b/rts/sm/HeapAlloc.h
new file mode 100644 (file)
index 0000000..c914b5d
--- /dev/null
@@ -0,0 +1,224 @@
+/* -----------------------------------------------------------------------------
+ *
+ * (c) The University of Glasgow 2006-2008
+ *
+ * The HEAP_ALLOCED() test.
+ *
+ * ---------------------------------------------------------------------------*/
+
+#ifndef SM_HEAP_ALLOC_H
+#define SM_HEAP_ALLOC_H
+
+#include "BeginPrivate.h"
+
+/* -----------------------------------------------------------------------------
+   The HEAP_ALLOCED() test.
+
+   HEAP_ALLOCED is called FOR EVERY SINGLE CLOSURE during GC.
+   It needs to be FAST.
+
+   See wiki commentary at
+     http://ghc.haskell.org/trac/ghc/wiki/Commentary/HeapAlloced
+
+   Implementation of HEAP_ALLOCED
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+   Since heap is allocated in chunks of megablocks (MBLOCK_SIZE), we
+   can just use a table to record which megablocks in the address
+   space belong to the heap.  On a 32-bit machine, with 1Mb
+   megablocks, using 8 bits for each entry in the table, the table
+   requires 4k.  Lookups during GC will be fast, because the table
+   will be quickly cached (indeed, performance measurements showed no
+   measurable difference between doing the table lookup and using a
+   constant comparison).
+
+   On 64-bit machines, we have two possibilities. One is to request
+   a single chunk of address space that we deem "large enough"
+   (currently 1TB, could easily be extended to, say 16TB or more).
+   Memory from that chunk is GC memory, everything else is not. This
+   case is tricky in that it requires support from the OS to allocate
+   address space without allocating memory (in practice, all modern
+   OSes do this). It's also tricky in that it is the only case where
+   a successful HEAP_ALLOCED(p) check can trigger a segfault when
+   accessing p (and for debugging purposes, it will).
+
+   Alternatively, the older implementation caches one 12-bit block map
+   that describes 4096 megablocks or 4GB of memory. If HEAP_ALLOCED is
+   called for an address that is not in the cache, it calls
+   slowIsHeapAlloced (see MBlock.c) which will find the block map for
+   the 4GB block in question.
+   -------------------------------------------------------------------------- */
+
+#ifdef USE_LARGE_ADDRESS_SPACE
+
+extern W_ mblock_address_space_begin;
+# define MBLOCK_SPACE_SIZE      ((StgWord)1 << 40) /* 1 TB */
+# define HEAP_ALLOCED(p)        ((W_)(p) >= mblock_address_space_begin && \
+                                 (W_)(p) < (mblock_address_space_begin +  \
+                                            MBLOCK_SPACE_SIZE))
+# define HEAP_ALLOCED_GC(p)     HEAP_ALLOCED(p)
+
+#elif SIZEOF_VOID_P == 4
+extern StgWord8 mblock_map[];
+
+/* On a 32-bit machine a 4KB table is always sufficient */
+# define MBLOCK_MAP_SIZE        4096
+# define MBLOCK_MAP_ENTRY(p)    ((StgWord)(p) >> MBLOCK_SHIFT)
+# define HEAP_ALLOCED(p)        mblock_map[MBLOCK_MAP_ENTRY(p)]
+# define HEAP_ALLOCED_GC(p)     HEAP_ALLOCED(p)
+
+/* -----------------------------------------------------------------------------
+   HEAP_ALLOCED for 64-bit machines (without LARGE_ADDRESS_SPACE).
+
+ Here are some cache layout options:
+
+ [1]
+ 16KB cache of 16-bit entries, 1MB lines (capacity 8GB)
+  mblock size =          20 bits
+  entries   =     8192   13 bits
+  line size =             0 bits (1 bit of value)
+  tag size  =            15 bits
+                       = 48 bits
+
+ [2]
+ 32KB cache of 16-bit entries, 4MB lines (capacity 32GB)
+  mblock size =          20 bits
+  entries   =    16384   14 bits
+  line size =             2 bits (4 bits of value)
+  tag size  =            12 bits
+                       = 48 bits
+
+ [3]
+ 16KB cache of 16-bit entries, 2MB lines (capacity 16GB)
+  mblock size =          20 bits
+  entries   =    8192    13 bits
+  line size =             1 bits (2 bits of value)
+  tag size  =            14 bits
+                       = 48 bits
+
+ [4]
+ 4KB cache of 32-bit entries, 16MB lines (capacity 16GB)
+  mblock size =          20 bits
+  entries   =     1024   10 bits
+  line size =             4 bits (16 bits of value)
+  tag size  =            14 bits
+                       = 48 bits
+
+ [5]
+ 4KB cache of 64-bit entries, 32MB lines (capacity 16GB)
+  mblock size =          20 bits
+  entries   =     512     9 bits
+  line size =             5 bits (32 bits of value)
+  tag size  =            14 bits
+                       = 48 bits
+
+ We actually use none of the above.  After much experimentation it was
+ found that optimising the lookup is the most important factor,
+ followed by reducing the number of misses.  To that end, we use a
+ variant of [1] in which each cache entry is ((mblock << 1) + value)
+ where value is 0 for non-heap and 1 for heap.  The cache entries can
+ be 32 bits, since the mblock number is 48-20 = 28 bits, and we need
+ 1 bit for the value.  The cache can be as big as we like, but
+ currently we use 8k entries, giving us 8GB capacity.
+
+ ---------------------------------------------------------------------------- */
+
+#elif SIZEOF_VOID_P == 8
+
+#define MBC_LINE_BITS 0
+#define MBC_TAG_BITS 15
+
+#if x86_64_HOST_ARCH
+// 32bits are enough for 'entry' as modern amd64 boxes have
+// only 48bit sized virtual addres.
+typedef StgWord32 MbcCacheLine;
+#else
+// 32bits is not enough here as some arches (like ia64) use
+// upper address bits to distinct memory areas.
+typedef StgWord64 MbcCacheLine;
+#endif
+
+typedef StgWord8  MBlockMapLine;
+
+#define MBLOCK_MAP_LINE(p)  (((StgWord)p & 0xffffffff) >> (MBLOCK_SHIFT + MBC_LINE_BITS))
+
+#define MBC_LINE_SIZE  (1<<MBC_LINE_BITS)
+#define MBC_SHIFT      (48 - MBLOCK_SHIFT - MBC_LINE_BITS - MBC_TAG_BITS)
+#define MBC_ENTRIES    (1<<MBC_SHIFT)
+
+extern MbcCacheLine mblock_cache[];
+
+#define MBC_LINE(p) ((StgWord)p >> (MBLOCK_SHIFT + MBC_LINE_BITS))
+
+#define MBLOCK_MAP_ENTRIES  (1 << (32 - MBLOCK_SHIFT - MBC_LINE_BITS))
+
+typedef struct {
+    StgWord32    addrHigh32;
+    MBlockMapLine lines[MBLOCK_MAP_ENTRIES];
+} MBlockMap;
+
+extern W_ mpc_misses;
+
+StgBool HEAP_ALLOCED_miss(StgWord mblock, void *p);
+
+INLINE_HEADER
+StgBool HEAP_ALLOCED(void *p)
+{
+    StgWord mblock;
+    nat entry_no;
+    MbcCacheLine entry, value;
+
+    mblock   = (StgWord)p >> MBLOCK_SHIFT;
+    entry_no = mblock & (MBC_ENTRIES-1);
+    entry    = mblock_cache[entry_no];
+    value    = entry ^ (mblock << 1);
+    // this formulation coaxes gcc into prioritising the value==1
+    // case, which we expect to be the most common.
+    // __builtin_expect() didn't have any useful effect (gcc-4.3.0).
+    if (value == 1) {
+        return 1;
+    } else if (value == 0) {
+        return 0;
+    } else {
+        // putting the rest out of line turned out to be a slight
+        // performance improvement:
+        return HEAP_ALLOCED_miss(mblock,p);
+    }
+}
+
+// In the parallel GC, the cache itself is safe to *read*, and can be
+// updated atomically, but we need to place a lock around operations
+// that touch the MBlock map.
+INLINE_HEADER
+StgBool HEAP_ALLOCED_GC(void *p)
+{
+    StgWord mblock;
+    nat entry_no;
+    MbcCacheLine entry, value;
+    StgBool b;
+
+    mblock   = (StgWord)p >> MBLOCK_SHIFT;
+    entry_no = mblock & (MBC_ENTRIES-1);
+    entry    = mblock_cache[entry_no];
+    value    = entry ^ (mblock << 1);
+    if (value == 1) {
+        return 1;
+    } else if (value == 0) {
+        return 0;
+    } else {
+        // putting the rest out of line turned out to be a slight
+        // performance improvement:
+        ACQUIRE_SPIN_LOCK(&gc_alloc_block_sync);
+        b = HEAP_ALLOCED_miss(mblock,p);
+        RELEASE_SPIN_LOCK(&gc_alloc_block_sync);
+        return b;
+    }
+}
+
+#else
+# error HEAP_ALLOCED not defined
+#endif
+
+#include "EndPrivate.h"
+
+#endif /* SM_HEAP_ALLOC_H */
index f626e1f..c77a9e0 100644 (file)
@@ -23,9 +23,320 @@ W_ mblocks_allocated = 0;
 W_ mpc_misses = 0;
 
 /* -----------------------------------------------------------------------------
-   The MBlock Map: provides our implementation of HEAP_ALLOCED()
+   The MBlock Map: provides our implementation of HEAP_ALLOCED() and the
+   utilities to walk the really allocated (thus accessible without risk of
+   segfault) heap
    -------------------------------------------------------------------------- */
 
+/*
+  There are two different cases here: either we use "large address
+  space" (which really means two-step allocation), so we have to
+  manage which memory is good (= accessible without fear of segfault)
+  and which is not owned by us, or we use the older method and get
+  good memory straight from the system.
+
+  Both code paths need to provide:
+
+  void *getFirstMBlock(void ** state)
+      return the first (lowest address) mblock
+      that was actually committed
+
+  void *getNextMBlock(void ** state, void * mblock)
+      return the first (lowest address) mblock
+      that was committed, after the given one
+
+  For both these calls, @state is an in-out parameter that points to
+  an opaque state threading the calls togheter. The calls should only
+  be used in an interation fashion. Pass NULL if @state is not
+  interesting,or pass a pointer to NULL if you don't have a state.
+
+  void *getCommittedMBlocks(nat n)
+      return @n new mblocks, ready to be used (reserved and committed)
+
+  void *decommitMBlocks(char *addr, nat n)
+      release memory for @n mblocks, starting at the given address
+
+  void releaseFreeMemory()
+      potentially release any address space that was associated
+      with recently decommitted blocks
+*/
+
+#ifdef USE_LARGE_ADDRESS_SPACE
+
+// Large address space means we use two-step allocation: reserve
+// something large upfront, and then commit as needed
+// (This is normally only useful on 64-bit, where we can assume
+// that reserving 1TB is possible)
+//
+// There is no block map in this case, but there is a free list
+// of blocks that were committed and decommitted at least once,
+// which we use to choose which block to commit next in the already
+// reserved space.
+//
+// We cannot let the OS choose it as we do in the
+// non large address space case, because the committing wants to
+// know the exact address upfront.
+//
+// The free list is coalesced and ordered, which means that
+// allocate and free are worst-case O(n), but benchmarks have shown
+// that this is not a significant problem, because large (>=2MB)
+// allocations are infrequent and their time is mostly insignificant
+// compared to the time to use that memory.
+//
+// The free list is stored in malloc()'d memory, unlike the other free
+// lists in BlockAlloc.c which are stored in block descriptors,
+// because we cannot touch the contents of decommitted mblocks.
+
+typedef struct free_list {
+    struct free_list *prev;
+    struct free_list *next;
+    W_ address;
+    W_ size;
+} free_list;
+
+static free_list *free_list_head;
+static W_ mblock_high_watermark;
+W_ mblock_address_space_begin = 0;
+
+static void *getAllocatedMBlock(free_list **start_iter, W_ startingAt)
+{
+    free_list *iter;
+    W_ p = startingAt;
+
+    for (iter = *start_iter; iter != NULL; iter = iter->next)
+    {
+        if (p < iter->address)
+            break;
+
+        if (p == iter->address)
+            p += iter->size;
+    }
+
+    *start_iter = iter;
+
+    if (p >= mblock_high_watermark)
+        return NULL;
+
+    return (void*)p;
+}
+
+void * getFirstMBlock(void **state)
+{
+    free_list *fake_state;
+    free_list **casted_state;
+
+    if (state)
+        casted_state = (free_list**)state;
+    else
+        casted_state = &fake_state;
+
+    *casted_state = free_list_head;
+    return getAllocatedMBlock(casted_state, mblock_address_space_begin);
+}
+
+void * getNextMBlock(void **state, void *mblock)
+{
+    free_list *fake_state = free_list_head;
+    free_list **casted_state;
+
+    if (state)
+        casted_state = (free_list**)state;
+    else
+        casted_state = &fake_state;
+
+    return getAllocatedMBlock(casted_state, (W_)mblock + MBLOCK_SIZE);
+}
+
+static void *getReusableMBlocks(nat n)
+{
+    struct free_list *iter;
+    W_ size = MBLOCK_SIZE * (W_)n;
+
+    for (iter = free_list_head; iter != NULL; iter = iter->next) {
+        void *addr;
+
+        if (iter->size < size)
+            continue;
+
+        addr = (void*)iter->address;
+        iter->address += size;
+        iter->size -= size;
+        if (iter->size == 0) {
+            struct free_list *prev, *next;
+
+            prev = iter->prev;
+            next = iter->next;
+            if (prev == NULL) {
+                ASSERT(free_list_head == iter);
+                free_list_head = next;
+            } else {
+                prev->next = next;
+            }
+            if (next != NULL) {
+                next->prev = prev;
+            }
+            stgFree(iter);
+        }
+
+        osCommitMemory(addr, size);
+        return addr;
+    }
+
+    return NULL;
+}
+
+static void *getFreshMBlocks(nat n)
+{
+    W_ size = MBLOCK_SIZE * (W_)n;
+    void *addr = (void*)mblock_high_watermark;
+
+    if (mblock_high_watermark + size >
+        mblock_address_space_begin + MBLOCK_SPACE_SIZE)
+    {
+        // whoa, 1 TB of heap?
+        errorBelch("out of memory");
+        stg_exit(EXIT_HEAPOVERFLOW);
+    }
+
+    osCommitMemory(addr, size);
+    mblock_high_watermark += size;
+    return addr;
+}
+
+static void *getCommittedMBlocks(nat n)
+{
+    void *p;
+
+    p = getReusableMBlocks(n);
+    if (p == NULL) {
+        p = getFreshMBlocks(n);
+    }
+
+    ASSERT(p != NULL && p != (void*)-1);
+    return p;
+}
+
+static void decommitMBlocks(char *addr, nat n)
+{
+    struct free_list *iter, *prev;
+    W_ size = MBLOCK_SIZE * (W_)n;
+    W_ address = (W_)addr;
+
+    osDecommitMemory(addr, size);
+
+    prev = NULL;
+    for (iter = free_list_head; iter != NULL; iter = iter->next)
+    {
+        prev = iter;
+
+        if (iter->address + iter->size < address)
+            continue;
+
+        if (iter->address + iter->size == address) {
+            iter->size += size;
+
+            if (address + size == mblock_high_watermark) {
+                mblock_high_watermark -= iter->size;
+                if (iter->prev) {
+                    iter->prev->next = NULL;
+                } else {
+                    ASSERT(iter == free_list_head);
+                    free_list_head = NULL;
+                }
+                stgFree(iter);
+                return;
+            }
+
+            if (iter->next &&
+                iter->next->address == iter->address + iter->size) {
+                struct free_list *next;
+
+                next = iter->next;
+                iter->size += next->size;
+                iter->next = next->next;
+
+                if (iter->next) {
+                    iter->next->prev = iter;
+
+                    /* We don't need to consolidate more */
+                    ASSERT(iter->next->address > iter->address + iter->size);
+                }
+
+                stgFree(next);
+            }
+            return;
+        } else if (address + size == iter->address) {
+            iter->address = address;
+            iter->size += size;
+
+            /* We don't need to consolidate backwards
+               (because otherwise it would have been handled by
+               the previous iteration) */
+            if (iter->prev) {
+                ASSERT(iter->prev->address + iter->prev->size < iter->address);
+            }
+            return;
+        } else {
+            struct free_list *new_iter;
+
+            /* All other cases have been handled */
+            ASSERT(iter->address > address + size);
+
+            new_iter = stgMallocBytes(sizeof(struct free_list), "freeMBlocks");
+            new_iter->address = address;
+            new_iter->size = size;
+            new_iter->next = iter;
+            new_iter->prev = iter->prev;
+            if (new_iter->prev) {
+                new_iter->prev->next = new_iter;
+            } else {
+                ASSERT(iter == free_list_head);
+                free_list_head = new_iter;
+            }
+            iter->prev = new_iter;
+            return;
+        }
+    }
+
+    /* We're past the last free list entry, so we must
+       be the highest allocation so far
+    */
+    ASSERT(address + size <= mblock_high_watermark);
+
+    /* Fast path the case of releasing high or all memory */
+    if (address + size == mblock_high_watermark) {
+        mblock_high_watermark -= size;
+    } else {
+        struct free_list *new_iter;
+
+        new_iter = stgMallocBytes(sizeof(struct free_list), "freeMBlocks");
+        new_iter->address = address;
+        new_iter->size = size;
+        new_iter->next = NULL;
+        new_iter->prev = prev;
+        if (new_iter->prev) {
+            ASSERT(new_iter->prev->next == NULL);
+            new_iter->prev->next = new_iter;
+        } else {
+            ASSERT(free_list_head == NULL);
+            free_list_head = new_iter;
+        }
+    }
+}
+
+void releaseFreeMemory(void)
+{
+    // This function exists for releasing address space
+    // on Windows 32 bit
+    //
+    // Do nothing if USE_LARGE_ADDRESS_SPACE, we never want
+    // to release address space
+
+    debugTrace(DEBUG_gc, "mblock_high_watermark: %p\n", mblock_high_watermark);
+}
+
+#else // !USE_LARGE_ADDRESS_SPACE
+
 #if SIZEOF_VOID_P == 4
 StgWord8 mblock_map[MBLOCK_MAP_SIZE]; // initially all zeros
 
@@ -108,6 +419,7 @@ setHeapAlloced(void *p, StgWord8 i)
         mblock_cache[entry_no] = (mblock << 1) + i;
     }
 }
+
 #endif
 
 static void
@@ -130,7 +442,7 @@ void * mapEntryToMBlock(nat i)
     return (void *)((StgWord)i << MBLOCK_SHIFT);
 }
 
-void * getFirstMBlock(void)
+void * getFirstMBlock(void **)
 {
     nat i;
 
@@ -140,7 +452,7 @@ void * getFirstMBlock(void)
     return NULL;
 }
 
-void * getNextMBlock(void *mblock)
+void * getNextMBlock(void **, void *mblock)
 {
     nat i;
 
@@ -152,7 +464,7 @@ void * getNextMBlock(void *mblock)
 
 #elif SIZEOF_VOID_P == 8
 
-void * getNextMBlock(void *p)
+void * getNextMBlock(void **, void *p)
 {
     MBlockMap *map;
     nat off, j;
@@ -189,7 +501,7 @@ void * getNextMBlock(void *p)
     return NULL;
 }
 
-void * getFirstMBlock(void)
+void * getFirstMBlock(void **)
 {
     MBlockMap *map = mblock_maps[0];
     nat line_no, off;
@@ -210,7 +522,38 @@ void * getFirstMBlock(void)
     return NULL;
 }
 
-#endif // SIZEOF_VOID_P
+#endif // SIZEOF_VOID_P == 8
+
+static void *getCommittedMBlocks(nat n)
+{
+    // The OS layer returns committed memory directly
+    void *ret = osGetMBlocks(n);
+    nat i;
+
+    // fill in the table
+    for (i = 0; i < n; i++) {
+        markHeapAlloced( (StgWord8*)ret + i * MBLOCK_SIZE );
+    }
+
+    return ret;
+}
+
+static void decommitMBlocks(void *p, nat n)
+{
+    osFreeMBlocks(p, n);
+    nat i;
+
+    for (i = 0; i < n; i++) {
+        markHeapUnalloced( (StgWord8*)p + i * MBLOCK_SIZE );
+    }
+}
+
+void releaseFreeMemory(void)
+{
+    osReleaseFreeMemory();
+}
+
+#endif /* !USE_LARGE_ADDRESS_SPACE */
 
 /* -----------------------------------------------------------------------------
    Allocate new mblock(s)
@@ -228,18 +571,12 @@ getMBlock(void)
 void *
 getMBlocks(nat n)
 {
-    nat i;
     void *ret;
 
-    ret = osGetMBlocks(n);
+    ret = getCommittedMBlocks(n);
 
     debugTrace(DEBUG_gc, "allocated %d megablock(s) at %p",n,ret);
 
-    // fill in the table
-    for (i = 0; i < n; i++) {
-        markHeapAlloced( (StgWord8*)ret + i * MBLOCK_SIZE );
-    }
-
     mblocks_allocated += n;
     peak_mblocks_allocated = stg_max(peak_mblocks_allocated, mblocks_allocated);
 
@@ -249,17 +586,11 @@ getMBlocks(nat n)
 void
 freeMBlocks(void *addr, nat n)
 {
-    nat i;
-
     debugTrace(DEBUG_gc, "freeing %d megablock(s) at %p",n,addr);
 
     mblocks_allocated -= n;
 
-    for (i = 0; i < n; i++) {
-        markHeapUnalloced( (StgWord8*)addr + i * MBLOCK_SIZE );
-    }
-
-    osFreeMBlocks(addr, n);
+    decommitMBlocks(addr, n);
 }
 
 void
@@ -267,6 +598,22 @@ freeAllMBlocks(void)
 {
     debugTrace(DEBUG_gc, "freeing all megablocks");
 
+#ifdef USE_LARGE_ADDRESS_SPACE
+    {
+        struct free_list *iter, *next;
+
+        for (iter = free_list_head; iter != NULL; iter = next)
+        {
+            next = iter->next;
+            stgFree(iter);
+        }
+    }
+
+    osReleaseHeapMemory();
+
+    mblock_address_space_begin = (W_)-1;
+    mblock_high_watermark = (W_)-1;
+#else
     osFreeAllMBlocks();
 
 #if SIZEOF_VOID_P == 8
@@ -276,13 +623,23 @@ freeAllMBlocks(void)
     }
     stgFree(mblock_maps);
 #endif
+
+#endif
 }
 
 void
 initMBlocks(void)
 {
     osMemInit();
-#if SIZEOF_VOID_P == 8
+
+#ifdef USE_LARGE_ADDRESS_SPACE
+    {
+        void *addr = osReserveHeapMemory();
+
+        mblock_address_space_begin = (W_)addr;
+        mblock_high_watermark = (W_)addr;
+    }
+#elif SIZEOF_VOID_P == 8
     memset(mblock_cache,0xff,sizeof(mblock_cache));
 #endif
 }
index db704fc..9a6ccdd 100644 (file)
@@ -20,6 +20,47 @@ W_ getPageSize (void);
 StgWord64 getPhysicalMemorySize (void);
 void setExecutable (void *p, W_ len, rtsBool exec);
 
+#ifdef USE_LARGE_ADDRESS_SPACE
+
+/*
+  If "large address space" is enabled, we allocate memory in two
+  steps: first we request some address space, and then we request some
+  memory in it. This allows us to ask for much more address space that
+  we will ever need, which keeps everything nice and consecutive.
+*/
+
+// Reserve the large address space blob, and return the address that
+// the OS has chosen for it.  It is not safe to access the memory
+// pointed to by the return value, until that memory is committed
+// using osCommitMemory().
+//
+// This function is called once when the block allocator is initialized.
+void *osReserveHeapMemory(void);
+
+// Commit (allocate memory for) a piece of address space, which must
+// be within the previously reserved space After this call, it is safe
+// to access @p up to @len bytes.
+//
+// There is no guarantee on the contents of the memory pointed to by
+// @p, in particular it must not be assumed to contain all zeros.
+void osCommitMemory(void *p, W_ len);
+
+// Decommit (release backing memory for) a piece of address space,
+// which must be within the previously reserve space and must have
+// been previously committed After this call, it is again unsafe to
+// access @p (up to @len bytes), but there is no guarantee that the
+// memory will be released to the system (as far as eg. RSS statistics
+// from top are concerned).
+void osDecommitMemory(void *p, W_ len);
+
+// Release the address space previously obtained and undo the effects of
+// osReserveHeapMemory
+//
+// This function is called once, when the block allocator is deinitialized
+// before the program terminates.
+void osReleaseHeapMemory(void);
+#endif
+
 #include "EndPrivate.h"
 
 #endif /* SM_OSMEM_H */
index afa5113..716171b 100644 (file)
@@ -8,6 +8,7 @@
 
 #include "Rts.h"
 #include "sm/OSMem.h"
+#include "sm/HeapAlloc.h"
 #include "RtsUtils.h"
 
 #if HAVE_WINDOWS_H
@@ -28,7 +29,11 @@ typedef struct block_rec_ {
 
 /* allocs are kept in ascending order, and are the memory regions as
    returned by the OS as we need to have matching VirtualAlloc and
-   VirtualFree calls. */
+   VirtualFree calls.
+
+   If USE_LARGE_ADDRESS_SPACE is defined, this list will contain only
+   one element.
+*/
 static alloc_rec* allocs = NULL;
 
 /* free_blocks are kept in ascending order, and adjacent blocks are merged */
@@ -207,12 +212,9 @@ osGetMBlocks(nat n) {
     return ret;
 }
 
-void osFreeMBlocks(char *addr, nat n)
+static void decommitBlocks(char *addr, W_ nBytes)
 {
     alloc_rec *p;
-    W_ nBytes = (W_)n * MBLOCK_SIZE;
-
-    insertFree(addr, nBytes);
 
     p = allocs;
     while ((p != NULL) && (addr >= (p->base + p->size))) {
@@ -243,6 +245,14 @@ void osFreeMBlocks(char *addr, nat n)
     }
 }
 
+void osFreeMBlocks(char *addr, nat n)
+{
+    W_ nBytes = (W_)n * MBLOCK_SIZE;
+
+    insertFree(addr, nBytes);
+    decommitBlocks(addr, nBytes);
+}
+
 void osReleaseFreeMemory(void)
 {
     alloc_rec *prev_a, *a;
@@ -414,3 +424,60 @@ void setExecutable (void *p, W_ len, rtsBool exec)
         stg_exit(EXIT_FAILURE);
     }
 }
+
+#ifdef USE_LARGE_ADDRESS_SPACE
+
+static void* heap_base = NULL;
+
+void *osReserveHeapMemory (void)
+{
+    void *start;
+
+    heap_base = VirtualAlloc(NULL, MBLOCK_SPACE_SIZE + MBLOCK_SIZE,
+                              MEM_RESERVE, PAGE_READWRITE);
+    if (heap_base == NULL) {
+        if (GetLastError() == ERROR_NOT_ENOUGH_MEMORY) {
+            errorBelch("out of memory");
+        } else {
+            sysErrorBelch(
+                "osReserveHeapMemory: VirtualAlloc MEM_RESERVE %llu bytes failed",
+                MBLOCK_SPACE_SIZE + MBLOCK_SIZE);
+        }
+        stg_exit(EXIT_FAILURE);
+    }
+
+    // VirtualFree MEM_RELEASE must always match a
+    // previous MEM_RESERVE call, in address and size
+    // so we necessarily leak some address space here,
+    // before and after the aligned area
+    // It is not a huge problem because we never commit
+    // that memory
+    start = MBLOCK_ROUND_UP(heap_base);
+
+    return start;
+}
+
+void osCommitMemory (void *at, W_ size)
+{
+    void *temp;
+    temp = VirtualAlloc(at, size, MEM_COMMIT, PAGE_READWRITE);
+    if (temp == NULL) {
+        sysErrorBelch("osCommitMemory: VirtualAlloc MEM_COMMIT failed");
+        stg_exit(EXIT_FAILURE);
+    }
+}
+
+void osDecommitMemory (void *at, W_ size)
+{
+    if (!VirtualFree(at, size, MEM_DECOMMIT)) {
+        sysErrorBelch("osDecommitMemory: VirtualFree MEM_DECOMMIT failed");
+        stg_exit(EXIT_FAILURE);
+    }
+}
+
+void osReleaseHeapMemory (void)
+{
+    VirtualFree(heap_base, 0, MEM_RELEASE);
+}
+
+#endif
index 5e5782a..52de19c 100644 (file)
@@ -7,14 +7,14 @@ outofmem-prep::
 
 outofmem::
        @$(MAKE) outofmem-prep
-       @ulimit -v 10000000 2>/dev/null; ./outofmem || echo "exit($$?)"
+       @ulimit -m 10000000 2>/dev/null; ./outofmem || echo "exit($$?)"
 
 outofmem2-prep::
        '$(TEST_HC)' $(TEST_HC_OPTS) -v0 -rtsopts --make -fforce-recomp outofmem2.hs -o outofmem2
 
 outofmem2::
        @$(MAKE) outofmem2-prep
-       @ulimit -v 1000000 2>/dev/null; ./outofmem2 +RTS -M5m -RTS || echo "exit($$?)"
+       @ulimit -m 1000000 2>/dev/null; ./outofmem2 +RTS -M5m -RTS || echo "exit($$?)"
 
 T2615-prep:
        $(RM) libfoo_T2615.so
index 5be3634..0e891e8 100644 (file)
@@ -2,6 +2,13 @@ test('testblockalloc',
      [c_src, only_ways(['normal','threaded1']), extra_run_opts('+RTS -I0')], 
      compile_and_run, [''])
 
+test('testmblockalloc',
+     [c_src, only_ways(['normal','threaded1']), extra_run_opts('+RTS -I0')], 
+     compile_and_run, [''])
+# -I0 is important: the idle GC will run the memory leak detector,
+# which will crash because the mblocks we allocate are not in a state
+# the leak detector is expecting.
+
 
 # See bug #101, test requires +RTS -c (or equivalently +RTS -M<something>)
 # only GHCi triggers the bug, but we run the test all ways for completeness.
index 42a4696..dca02c7 100644 (file)
@@ -1 +1 @@
-outofmem: out of memory (requested 2148532224 bytes)
+outofmem: out of memory
diff --git a/testsuite/tests/rts/testmblockalloc.c b/testsuite/tests/rts/testmblockalloc.c
new file mode 100644 (file)
index 0000000..df03658
--- /dev/null
@@ -0,0 +1,75 @@
+#include "Rts.h"
+
+#include <stdio.h>
+
+// 16 * 64 == max 1GB
+const int MAXALLOC = 16;
+const int ARRSIZE  = 64;
+
+const int LOOPS    = 1000;
+const int SEED     = 0xf00f00;
+
+extern lnat mblocks_allocated;
+
+int main (int argc, char *argv[])
+{
+    int i, j, b;
+
+    void *a[ARRSIZE];
+    nat sizes[ARRSIZE];
+
+    srand(SEED);
+
+    {
+        RtsConfig conf = defaultRtsConfig;
+        conf.rts_opts_enabled = RtsOptsAll;
+        hs_init_ghc(&argc, &argv, conf);
+    }
+
+   // repeatedly sweep though the array, allocating new random-sized
+   // objects and deallocating the old ones.
+   for (i=0; i < LOOPS; i++)
+   {
+       for (j=0; j < ARRSIZE; j++)
+       {
+           if (i > 0)
+           {
+               freeMBlocks(a[j], sizes[j]);
+           }
+           b = (rand() % MAXALLOC) + 1;
+           a[j] = getMBlocks(b);
+           sizes[j] = b;
+       }
+   }
+
+   releaseFreeMemory();
+
+   for (j=0; j < ARRSIZE; j++)
+   {
+       freeMBlocks(a[j], sizes[j]);
+   }
+
+   releaseFreeMemory();
+
+    // this time, sweep forwards allocating new blocks, and then
+    // backwards deallocating them.
+    for (i=0; i < LOOPS; i++)
+    {
+        for (j=0; j < ARRSIZE; j++)
+        {
+            b = (rand() % MAXALLOC) + 1;
+            a[j] = getMBlocks(b);
+            sizes[j] = b;
+        }
+        for (j=ARRSIZE-1; j >= 0; j--)
+        {
+            freeMBlocks(a[j], sizes[j]);
+        }
+    }
+
+    releaseFreeMemory();
+
+    hs_exit(); // will do a memory leak test
+
+    exit(0);
+}