88#
99
1010import bisect
11+ from collections import defaultdict
1112import mmap
1213import os
1314import sys
2829 import _winapi
2930
3031 class Arena (object ):
32+ """
33+ A shared memory area backed by anonymous memory (Windows).
34+ """
3135
3236 _rand = tempfile ._RandomNameSequence ()
3337
@@ -52,6 +56,7 @@ def __getstate__(self):
5256
5357 def __setstate__ (self , state ):
5458 self .size , self .name = self ._state = state
59+ # Reopen existing mmap
5560 self .buffer = mmap .mmap (- 1 , self .size , tagname = self .name )
5661 # XXX Temporarily preventing buildbot failures while determining
5762 # XXX the correct long-term fix. See issue 23060
@@ -60,6 +65,10 @@ def __setstate__(self, state):
6065else :
6166
6267 class Arena (object ):
68+ """
69+ A shared memory area backed by a temporary file (POSIX).
70+ """
71+
6372 if sys .platform == 'linux' :
6473 _dir_candidates = ['/dev/shm' ]
6574 else :
@@ -69,6 +78,8 @@ def __init__(self, size, fd=-1):
6978 self .size = size
7079 self .fd = fd
7180 if fd == - 1 :
81+ # Arena is created anew (if fd != -1, it means we're coming
82+ # from rebuild_arena() below)
7283 self .fd , name = tempfile .mkstemp (
7384 prefix = 'pym-%d-' % os .getpid (),
7485 dir = self ._choose_dir (size ))
@@ -103,37 +114,82 @@ def rebuild_arena(size, dupfd):
103114
104115class Heap (object ):
105116
117+ # Minimum malloc() alignment
106118 _alignment = 8
107119
120+ _DISCARD_FREE_SPACE_LARGER_THAN = 4 * 1024 ** 2 # 4 MB
121+ _DOUBLE_ARENA_SIZE_UNTIL = 4 * 1024 ** 2
122+
108123 def __init__ (self , size = mmap .PAGESIZE ):
109124 self ._lastpid = os .getpid ()
110125 self ._lock = threading .Lock ()
126+ # Current arena allocation size
111127 self ._size = size
128+ # A sorted list of available block sizes in arenas
112129 self ._lengths = []
130+
131+ # Free block management:
132+ # - map each block size to a list of `(Arena, start, stop)` blocks
113133 self ._len_to_seq = {}
134+ # - map `(Arena, start)` tuple to the `(Arena, start, stop)` block
135+ # starting at that offset
114136 self ._start_to_block = {}
137+ # - map `(Arena, stop)` tuple to the `(Arena, start, stop)` block
138+ # ending at that offset
115139 self ._stop_to_block = {}
116- self ._allocated_blocks = set ()
140+
141+ # Map arenas to their `(Arena, start, stop)` blocks in use
142+ self ._allocated_blocks = defaultdict (set )
117143 self ._arenas = []
118- # list of pending blocks to free - see free() comment below
144+
145+ # List of pending blocks to free - see comment in free() below
119146 self ._pending_free_blocks = []
120147
148+ # Statistics
149+ self ._n_mallocs = 0
150+ self ._n_frees = 0
151+
121152 @staticmethod
122153 def _roundup (n , alignment ):
123154 # alignment must be a power of 2
124155 mask = alignment - 1
125156 return (n + mask ) & ~ mask
126157
158+ def _new_arena (self , size ):
159+ # Create a new arena with at least the given *size*
160+ length = self ._roundup (max (self ._size , size ), mmap .PAGESIZE )
161+ # We carve larger and larger arenas, for efficiency, until we
162+ # reach a large-ish size (roughly L3 cache-sized)
163+ if self ._size < self ._DOUBLE_ARENA_SIZE_UNTIL :
164+ self ._size *= 2
165+ util .info ('allocating a new mmap of length %d' , length )
166+ arena = Arena (length )
167+ self ._arenas .append (arena )
168+ return (arena , 0 , length )
169+
170+ def _discard_arena (self , arena ):
171+ # Possibly delete the given (unused) arena
172+ length = arena .size
173+ # Reusing an existing arena is faster than creating a new one, so
174+ # we only reclaim space if it's large enough.
175+ if length < self ._DISCARD_FREE_SPACE_LARGER_THAN :
176+ return
177+ blocks = self ._allocated_blocks .pop (arena )
178+ assert not blocks
179+ del self ._start_to_block [(arena , 0 )]
180+ del self ._stop_to_block [(arena , length )]
181+ self ._arenas .remove (arena )
182+ seq = self ._len_to_seq [length ]
183+ seq .remove ((arena , 0 , length ))
184+ if not seq :
185+ del self ._len_to_seq [length ]
186+ self ._lengths .remove (length )
187+
127188 def _malloc (self , size ):
128189 # returns a large enough block -- it might be much larger
129190 i = bisect .bisect_left (self ._lengths , size )
130191 if i == len (self ._lengths ):
131- length = self ._roundup (max (self ._size , size ), mmap .PAGESIZE )
132- self ._size *= 2
133- util .info ('allocating a new mmap of length %d' , length )
134- arena = Arena (length )
135- self ._arenas .append (arena )
136- return (arena , 0 , length )
192+ return self ._new_arena (size )
137193 else :
138194 length = self ._lengths [i ]
139195 seq = self ._len_to_seq [length ]
@@ -146,8 +202,8 @@ def _malloc(self, size):
146202 del self ._stop_to_block [(arena , stop )]
147203 return block
148204
149- def _free (self , block ):
150- # free location and try to merge with neighbours
205+ def _add_free_block (self , block ):
206+ # make block available and try to merge with its neighbours in the arena
151207 (arena , start , stop ) = block
152208
153209 try :
@@ -191,15 +247,23 @@ def _absorb(self, block):
191247
192248 return start , stop
193249
250+ def _remove_allocated_block (self , block ):
251+ arena , start , stop = block
252+ blocks = self ._allocated_blocks [arena ]
253+ blocks .remove ((start , stop ))
254+ if not blocks :
255+ # Arena is entirely free, discard it from this process
256+ self ._discard_arena (arena )
257+
194258 def _free_pending_blocks (self ):
195259 # Free all the blocks in the pending list - called with the lock held.
196260 while True :
197261 try :
198262 block = self ._pending_free_blocks .pop ()
199263 except IndexError :
200264 break
201- self ._allocated_blocks . remove (block )
202- self ._free (block )
265+ self ._add_free_block (block )
266+ self ._remove_allocated_block (block )
203267
204268 def free (self , block ):
205269 # free a block returned by malloc()
@@ -210,7 +274,7 @@ def free(self, block):
210274 # immediately, the block is added to a list of blocks to be freed
211275 # synchronously sometimes later from malloc() or free(), by calling
212276 # _free_pending_blocks() (appending and retrieving from a list is not
213- # strictly thread-safe but under cPython it's atomic thanks to the GIL).
277+ # strictly thread-safe but under CPython it's atomic thanks to the GIL).
214278 if os .getpid () != self ._lastpid :
215279 raise ValueError (
216280 "My pid ({0:n}) is not last pid {1:n}" .format (
@@ -222,9 +286,10 @@ def free(self, block):
222286 else :
223287 # we hold the lock
224288 try :
289+ self ._n_frees += 1
225290 self ._free_pending_blocks ()
226- self ._allocated_blocks . remove (block )
227- self ._free (block )
291+ self ._add_free_block (block )
292+ self ._remove_allocated_block (block )
228293 finally :
229294 self ._lock .release ()
230295
@@ -237,18 +302,21 @@ def malloc(self, size):
237302 if os .getpid () != self ._lastpid :
238303 self .__init__ () # reinitialize after fork
239304 with self ._lock :
305+ self ._n_mallocs += 1
306+ # allow pending blocks to be marked available
240307 self ._free_pending_blocks ()
241- size = self ._roundup (max (size ,1 ), self ._alignment )
308+ size = self ._roundup (max (size , 1 ), self ._alignment )
242309 (arena , start , stop ) = self ._malloc (size )
243- new_stop = start + size
244- if new_stop < stop :
245- self ._free ((arena , new_stop , stop ))
246- block = (arena , start , new_stop )
247- self ._allocated_blocks .add (block )
248- return block
310+ real_stop = start + size
311+ if real_stop < stop :
312+ # if the returned block is larger than necessary, mark
313+ # the remainder available
314+ self ._add_free_block ((arena , real_stop , stop ))
315+ self ._allocated_blocks [arena ].add ((start , real_stop ))
316+ return (arena , start , real_stop )
249317
250318#
251- # Class representing a chunk of an mmap -- can be inherited by child process
319+ # Class wrapping a block allocated out of a Heap -- can be inherited by child process
252320#
253321
254322class BufferWrapper (object ):
0 commit comments