[pypy-commit] pypy use-mmap: Trying to use mmap() instead of malloc() to get arenas. Advantage: munmap()

arigo pypy.commits at gmail.com
Wed Jul 6 11:57:12 EDT 2016


Author: Armin Rigo <arigo at tunes.org>
Branch: use-mmap
Changeset: r85579:f63cad3be093
Date: 2016-07-06 16:43 +0200
http://bitbucket.org/pypy/pypy/changeset/f63cad3be093/

Log:	Trying to use mmap() instead of malloc() to get arenas. Advantage:
	munmap() really returns the 256K/512K of memory to the system.

diff --git a/rpython/memory/gc/incminimark.py b/rpython/memory/gc/incminimark.py
--- a/rpython/memory/gc/incminimark.py
+++ b/rpython/memory/gc/incminimark.py
@@ -727,9 +727,10 @@
         return llmemory.cast_adr_to_ptr(obj, llmemory.GCREF)
 
 
-    def collect(self, gen=2):
+    def collect(self, gen=3):
         """Do a minor (gen=0), start a major (gen=1), or do a full
-        major (gen>=2) collection."""
+        major (gen>=2) collection.  If gen>=3 then ask minimarkpage
+        to free now its cache of arenas."""
         if gen < 0:
             self._minor_collection()   # dangerous! no major GC cycle progress
         elif gen <= 1:
@@ -738,6 +739,8 @@
                 self.major_collection_step()
         else:
             self.minor_and_major_collection()
+            if gen >= 3:
+                self.ac.kill_dying_arenas()
         self.rrc_invoke_callback()
 
 
diff --git a/rpython/memory/gc/minimarkpage.py b/rpython/memory/gc/minimarkpage.py
--- a/rpython/memory/gc/minimarkpage.py
+++ b/rpython/memory/gc/minimarkpage.py
@@ -124,6 +124,16 @@
                                               flavor='raw', zero=True,
                                               immortal=True)
         #
+        # two lists of completely free arenas.  These arenas are
+        # pending to be returned to the OS.  They are first added to
+        # the 'dying_arenas' list when the major collection runs.  At
+        # the end, they are moved to the 'dead_arenas' list, and all
+        # arenas that were already in the 'dead_arenas' list at that
+        # point (from the previous major collection) are really
+        # returned to the OS.
+        self.dying_arenas = ARENA_NULL
+        self.dead_arenas = ARENA_NULL
+        #
         # the arena currently consumed; it must have at least one page
         # available, or be NULL.  The arena object that we point to is
         # not in any 'arenas_lists'.  We will consume all its pages before
@@ -244,7 +254,7 @@
 
 
     def _all_arenas(self):
-        """For testing.  Enumerates all arenas."""
+        """For testing.  Enumerates all not-completely-free arenas."""
         if self.current_arena:
             yield self.current_arena
         for arena in self.arenas_lists:
@@ -290,31 +300,58 @@
             for a in self._all_arenas():
                 assert a.nfreepages == 0
         #
-        # 'arena_base' points to the start of malloced memory; it might not
-        # be a page-aligned address
-        arena_base = llarena.arena_malloc(self.arena_size, False)
-        if not arena_base:
-            out_of_memory("out of memory: couldn't allocate the next arena")
-        arena_end = arena_base + self.arena_size
+        # Maybe we have a dying or a dead arena.
+        if self.dying_arenas:
+            arena = self.dying_arenas
+            self.dying_arenas = arena.nextarena
+        elif self.dead_arenas:
+            arena = self.dead_arenas
+            self.dead_arenas = arena.nextarena
+        else:
+            #
+            # 'arena_base' points to the start of malloced memory.  It might
+            # not be a page-aligned address depending on the underlying call
+            # (although with mmap() it should be).
+            arena_base = llarena.arena_mmap(self.arena_size)
+            if not arena_base:
+                out_of_memory("out of memory: couldn't allocate the next arena")
+            arena_end = arena_base + self.arena_size
+            #
+            # 'firstpage' points to the first unused page
+            firstpage = start_of_page(arena_base + self.page_size - 1,
+                                      self.page_size)
+            # 'npages' is the number of full pages just allocated
+            npages = (arena_end - firstpage) // self.page_size
+            #
+            # Allocate an ARENA object and initialize it
+            arena = lltype.malloc(ARENA, flavor='raw', track_allocation=False)
+            arena.base = arena_base
+            arena.totalpages = npages
         #
-        # 'firstpage' points to the first unused page
-        firstpage = start_of_page(arena_base + self.page_size - 1,
-                                  self.page_size)
-        # 'npages' is the number of full pages just allocated
-        npages = (arena_end - firstpage) // self.page_size
-        #
-        # Allocate an ARENA object and initialize it
-        arena = lltype.malloc(ARENA, flavor='raw', track_allocation=False)
-        arena.base = arena_base
         arena.nfreepages = 0        # they are all uninitialized pages
-        arena.totalpages = npages
-        arena.freepages = firstpage
-        self.num_uninitialized_pages = npages
+        arena.freepages = start_of_page(arena.base + self.page_size - 1,
+                                        self.page_size)
+        arena.nextarena = ARENA_NULL
+        self.num_uninitialized_pages = arena.totalpages
         self.current_arena = arena
         #
     allocate_new_arena._dont_inline_ = True
 
 
+    def kill_dying_arenas(self):
+        """Return to the OS all dead arenas, and then move the 'dying'
+        arenas to the 'dead' arenas list.
+        """
+        arena = self.dead_arenas
+        while arena:
+            nextarena = arena.nextarena
+            llarena.arena_munmap(arena.base, self.arena_size)
+            lltype.free(arena, flavor='raw', track_allocation=False)
+            arena = nextarena
+        self.dead_arenas = self.dying_arenas
+        self.dying_arenas = ARENA_NULL
+
+
     def mass_free_prepare(self):
         """Prepare calls to mass_free_incremental(): moves the chained lists
         into 'self.old_xxx'.
@@ -360,6 +397,7 @@
         if size_class >= 0:
             self._rehash_arenas_lists()
             self.size_class_with_old_pages = -1
+            self.kill_dying_arenas()
         #
         return True
 
@@ -394,9 +432,12 @@
                 #
                 if arena.nfreepages == arena.totalpages:
                     #
-                    # The whole arena is empty.  Free it.
-                    llarena.arena_free(arena.base)
-                    lltype.free(arena, flavor='raw', track_allocation=False)
+                    # The whole arena is empty.  Move it to the dying list.
+                    arena.nextarena = self.dying_arenas
+                    self.dying_arenas = arena
+                    llarena.arena_reset(arena.base,
+                                        llarena.RESET_WHOLE_ARENA,
+                                        0)
                     #
                 else:
                     # Insert 'arena' in the correct arenas_lists[n]
diff --git a/rpython/rlib/rmmap.py b/rpython/rlib/rmmap.py
--- a/rpython/rlib/rmmap.py
+++ b/rpython/rlib/rmmap.py
@@ -691,6 +691,22 @@
         m.setdata(res, map_size)
         return m
 
+    def allocate_memory_chunk(map_size):
+        # used by the memory allocator (in llarena.py, from minimarkpage.py)
+        flags = MAP_PRIVATE | MAP_ANONYMOUS
+        prot = PROT_READ | PROT_WRITE
+        if we_are_translated():
+            flags = NonConstant(flags)
+            prot = NonConstant(prot)
+        res = c_mmap_safe(rffi.cast(PTR, 0), map_size, prot, flags, -1, 0)
+        if res == rffi.cast(PTR, -1):
+            res = rffi.cast(PTR, 0)
+        return res
+
+    def free_memory_chunk(addr, map_size):
+        # used by the memory allocator (in llarena.py, from minimarkpage.py)
+        c_munmap_safe(addr, map_size)
+
     def alloc_hinted(hintp, map_size):
         flags = MAP_PRIVATE | MAP_ANONYMOUS
         prot = PROT_EXEC | PROT_READ | PROT_WRITE
diff --git a/rpython/rtyper/lltypesystem/llarena.py b/rpython/rtyper/lltypesystem/llarena.py
--- a/rpython/rtyper/lltypesystem/llarena.py
+++ b/rpython/rtyper/lltypesystem/llarena.py
@@ -13,13 +13,15 @@
 # it internally uses raw_malloc_usage() to estimate the number of bytes
 # it needs to reserve.
 
+RESET_WHOLE_ARENA = -909
+
 class ArenaError(Exception):
     pass
 
 class Arena(object):
     _count_arenas = 0
 
-    def __init__(self, nbytes, zero):
+    def __init__(self, nbytes, zero, mmaped=False):
         Arena._count_arenas += 1
         self._arena_index = Arena._count_arenas
         self.nbytes = nbytes
@@ -29,13 +31,14 @@
         self.freed = False
         self.protect_inaccessible = None
         self.reset(zero)
+        self.mmaped = mmaped
 
     def __repr__(self):
         return '<Arena #%d [%d bytes]>' % (self._arena_index, self.nbytes)
 
-    def reset(self, zero, start=0, size=None):
+    def reset(self, zero, start=0, size=RESET_WHOLE_ARENA):
         self.check()
-        if size is None:
+        if type(size) is int and size == RESET_WHOLE_ARENA:
             stop = self.nbytes
         else:
             stop = start + llmemory.raw_malloc_usage(size)
@@ -315,18 +318,33 @@
 # arena_new_view(ptr) is a no-op when translated, returns fresh view
 # on previous arena when run on top of llinterp
 
+# arena_mmap() and arena_munmap() are similar to arena_malloc(zero=True)
+# and arena_free(), but use the OS's mmap() function.
+
 def arena_malloc(nbytes, zero):
     """Allocate and return a new arena, optionally zero-initialized."""
     return Arena(nbytes, zero).getaddr(0)
 
 def arena_free(arena_addr):
     """Release an arena."""
+    _arena_free(arena_addr, mmaped=False)
+
+def _arena_free(arena_addr, mmaped):
     assert isinstance(arena_addr, fakearenaaddress)
     assert arena_addr.offset == 0
+    assert arena_addr.arena.mmaped == mmaped
     arena_addr.arena.reset(False)
     assert not arena_addr.arena.objectptrs
     arena_addr.arena.mark_freed()
 
+def arena_mmap(nbytes):
+    """Allocate a new arena using mmap()."""
+    return Arena(nbytes, zero=True, mmaped=True).getaddr(0)
+
+def arena_munmap(arena_addr, original_nbytes):
+    """Release an arena allocated with arena_mmap()."""
+    _arena_free(arena_addr, mmaped=True)
+
 def arena_reset(arena_addr, size, zero):
     """Free all objects in the arena, which can then be reused.
     This can also be used on a subrange of the arena.
@@ -335,6 +353,7 @@
       * 1: clear, optimized for a very large area of memory
       * 2: clear, optimized for a small or medium area of memory
       * 3: fill with garbage
+    If 'zero' is set to 0, then size can be RESET_WHOLE_ARENA.
     """
     arena_addr = getfakearenaaddress(arena_addr)
     arena_addr.arena.reset(zero, arena_addr.offset, size)
@@ -503,6 +522,26 @@
                   llfakeimpl=arena_free,
                   sandboxsafe=True)
 
+def llimpl_arena_mmap(nbytes):
+    from rpython.rlib import rmmap
+    return rffi.cast(llmemory.Address, rmmap.allocate_memory_chunk(nbytes))
+
+def llimpl_arena_munmap(addr, nbytes):
+    from rpython.rlib import rmmap
+    rmmap.free_memory_chunk(rffi.cast(rmmap.PTR, addr), nbytes)
+
+register_external(arena_mmap, [int], llmemory.Address,
+                  'll_arena.arena_mmap',
+                  llimpl=llimpl_arena_mmap,
+                  llfakeimpl=arena_mmap,
+                  sandboxsafe=True)
+
+register_external(arena_munmap, [llmemory.Address, int], None,
+                  'll_arena.arena_munmap',
+                  llimpl=llimpl_arena_munmap,
+                  llfakeimpl=arena_munmap,
+                  sandboxsafe=True)
+
 def llimpl_arena_reset(arena_addr, size, zero):
     if zero:
         if zero == 1:


More information about the pypy-commit mailing list