8
8
#
9
9
10
10
import bisect
11
+ from collections import defaultdict
11
12
import mmap
12
13
import os
13
14
import sys
28
29
import _winapi
29
30
30
31
class Arena (object ):
32
+ """
33
+ A shared memory area backed by anonymous memory (Windows).
34
+ """
31
35
32
36
_rand = tempfile ._RandomNameSequence ()
33
37
@@ -52,6 +56,7 @@ def __getstate__(self):
52
56
53
57
def __setstate__ (self , state ):
54
58
self .size , self .name = self ._state = state
59
+ # Reopen existing mmap
55
60
self .buffer = mmap .mmap (- 1 , self .size , tagname = self .name )
56
61
# XXX Temporarily preventing buildbot failures while determining
57
62
# XXX the correct long-term fix. See issue 23060
@@ -60,6 +65,10 @@ def __setstate__(self, state):
60
65
else :
61
66
62
67
class Arena (object ):
68
+ """
69
+ A shared memory area backed by a temporary file (POSIX).
70
+ """
71
+
63
72
if sys .platform == 'linux' :
64
73
_dir_candidates = ['/dev/shm' ]
65
74
else :
@@ -69,6 +78,8 @@ def __init__(self, size, fd=-1):
69
78
self .size = size
70
79
self .fd = fd
71
80
if fd == - 1 :
81
+ # Arena is created anew (if fd != -1, it means we're coming
82
+ # from rebuild_arena() below)
72
83
self .fd , name = tempfile .mkstemp (
73
84
prefix = 'pym-%d-' % os .getpid (),
74
85
dir = self ._choose_dir (size ))
@@ -103,37 +114,82 @@ def rebuild_arena(size, dupfd):
103
114
104
115
class Heap (object ):
105
116
117
+ # Minimum malloc() alignment
106
118
_alignment = 8
107
119
120
+ _DISCARD_FREE_SPACE_LARGER_THAN = 4 * 1024 ** 2 # 4 MB
121
+ _DOUBLE_ARENA_SIZE_UNTIL = 4 * 1024 ** 2
122
+
108
123
def __init__ (self , size = mmap .PAGESIZE ):
109
124
self ._lastpid = os .getpid ()
110
125
self ._lock = threading .Lock ()
126
+ # Current arena allocation size
111
127
self ._size = size
128
+ # A sorted list of available block sizes in arenas
112
129
self ._lengths = []
130
+
131
+ # Free block management:
132
+ # - map each block size to a list of `(Arena, start, stop)` blocks
113
133
self ._len_to_seq = {}
134
+ # - map `(Arena, start)` tuple to the `(Arena, start, stop)` block
135
+ # starting at that offset
114
136
self ._start_to_block = {}
137
+ # - map `(Arena, stop)` tuple to the `(Arena, start, stop)` block
138
+ # ending at that offset
115
139
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 )
117
143
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
119
146
self ._pending_free_blocks = []
120
147
148
+ # Statistics
149
+ self ._n_mallocs = 0
150
+ self ._n_frees = 0
151
+
121
152
@staticmethod
122
153
def _roundup (n , alignment ):
123
154
# alignment must be a power of 2
124
155
mask = alignment - 1
125
156
return (n + mask ) & ~ mask
126
157
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
+
127
188
def _malloc (self , size ):
128
189
# returns a large enough block -- it might be much larger
129
190
i = bisect .bisect_left (self ._lengths , size )
130
191
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 )
137
193
else :
138
194
length = self ._lengths [i ]
139
195
seq = self ._len_to_seq [length ]
@@ -146,8 +202,8 @@ def _malloc(self, size):
146
202
del self ._stop_to_block [(arena , stop )]
147
203
return block
148
204
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
151
207
(arena , start , stop ) = block
152
208
153
209
try :
@@ -191,15 +247,23 @@ def _absorb(self, block):
191
247
192
248
return start , stop
193
249
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
+
194
258
def _free_pending_blocks (self ):
195
259
# Free all the blocks in the pending list - called with the lock held.
196
260
while True :
197
261
try :
198
262
block = self ._pending_free_blocks .pop ()
199
263
except IndexError :
200
264
break
201
- self ._allocated_blocks . remove (block )
202
- self ._free (block )
265
+ self ._add_free_block (block )
266
+ self ._remove_allocated_block (block )
203
267
204
268
def free (self , block ):
205
269
# free a block returned by malloc()
@@ -210,7 +274,7 @@ def free(self, block):
210
274
# immediately, the block is added to a list of blocks to be freed
211
275
# synchronously sometimes later from malloc() or free(), by calling
212
276
# _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).
214
278
if os .getpid () != self ._lastpid :
215
279
raise ValueError (
216
280
"My pid ({0:n}) is not last pid {1:n}" .format (
@@ -222,9 +286,10 @@ def free(self, block):
222
286
else :
223
287
# we hold the lock
224
288
try :
289
+ self ._n_frees += 1
225
290
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 )
228
293
finally :
229
294
self ._lock .release ()
230
295
@@ -237,18 +302,21 @@ def malloc(self, size):
237
302
if os .getpid () != self ._lastpid :
238
303
self .__init__ () # reinitialize after fork
239
304
with self ._lock :
305
+ self ._n_mallocs += 1
306
+ # allow pending blocks to be marked available
240
307
self ._free_pending_blocks ()
241
- size = self ._roundup (max (size ,1 ), self ._alignment )
308
+ size = self ._roundup (max (size , 1 ), self ._alignment )
242
309
(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 )
249
317
250
318
#
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
252
320
#
253
321
254
322
class BufferWrapper (object ):
0 commit comments