/home/uke/oil/mycpp/mark_sweep_heap.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include "mycpp/mark_sweep_heap.h" |
2 | | |
3 | | #include <inttypes.h> // PRId64 |
4 | | #include <stdio.h> // dprintf() |
5 | | #include <stdlib.h> // getenv() |
6 | | #include <string.h> // strlen() |
7 | | #include <sys/time.h> // gettimeofday() |
8 | | #include <time.h> // clock_gettime(), CLOCK_PROCESS_CPUTIME_ID |
9 | | #include <unistd.h> // STDERR_FILENO |
10 | | |
11 | | #include "_build/detected-cpp-config.h" // for GC_TIMING |
12 | | #include "mycpp/gc_builtins.h" // StringToInt() |
13 | | #include "mycpp/gc_slab.h" |
14 | | |
15 | | // TODO: Remove this guard when we have separate binaries |
16 | | #if MARK_SWEEP |
17 | | |
18 | 77 | void MarkSweepHeap::Init() { |
19 | 77 | Init(1000); // collect at 1000 objects in tests |
20 | 77 | } |
21 | | |
22 | 79 | void MarkSweepHeap::Init(int gc_threshold) { |
23 | 79 | gc_threshold_ = gc_threshold; |
24 | | |
25 | 79 | char* e; |
26 | 79 | e = getenv("OILS_GC_THRESHOLD"); |
27 | 79 | if (e) { |
28 | 0 | int result; |
29 | 0 | if (StringToInt(e, strlen(e), 10, &result)) { |
30 | | // Override collection threshold |
31 | 0 | gc_threshold_ = result; |
32 | 0 | } |
33 | 0 | } |
34 | | |
35 | | // only for developers |
36 | 79 | e = getenv("_OILS_GC_VERBOSE"); |
37 | 79 | if (e && strcmp(e, "1") == 0) { |
38 | 0 | gc_verbose_ = true; |
39 | 0 | } |
40 | | |
41 | 79 | live_objs_.reserve(KiB(10)); |
42 | 79 | roots_.reserve(KiB(1)); // prevent resizing in common case |
43 | 79 | } |
44 | | |
45 | 2.07k | int MarkSweepHeap::MaybeCollect() { |
46 | | // Maybe collect BEFORE allocation, because the new object won't be rooted |
47 | | #if GC_ALWAYS |
48 | | int result = Collect(); |
49 | | #else |
50 | 2.07k | int result = -1; |
51 | 2.07k | if (num_live() > gc_threshold_) { |
52 | 9 | result = Collect(); |
53 | 9 | } |
54 | 2.07k | #endif |
55 | | |
56 | 2.07k | num_gc_points_++; // this is a manual collection point |
57 | 2.07k | return result; |
58 | 2.07k | } |
59 | | |
60 | | #if defined(BUMP_SMALL) |
61 | | #include "mycpp/bump_leak_heap.h" |
62 | | |
63 | | BumpLeakHeap gBumpLeak; |
64 | | #endif |
65 | | |
66 | | // Allocate and update stats |
67 | | // TODO: Make this interface nicer. |
68 | 16.9k | void* MarkSweepHeap::Allocate(size_t num_bytes, int* obj_id, int* pool_id) { |
69 | | // log("Allocate %d", num_bytes); |
70 | 16.9k | #ifndef NO_POOL_ALLOC |
71 | 16.9k | if (num_bytes <= pool1_.kMaxObjSize) { |
72 | 10.7k | *pool_id = 1; |
73 | 10.7k | return pool1_.Allocate(obj_id); |
74 | 10.7k | } |
75 | 6.24k | if (num_bytes <= pool2_.kMaxObjSize) { |
76 | 4.79k | *pool_id = 2; |
77 | 4.79k | return pool2_.Allocate(obj_id); |
78 | 4.79k | } |
79 | 1.44k | *pool_id = 0; // malloc(), not a pool |
80 | 1.44k | #endif |
81 | | |
82 | | // Does the pool allocator approximate a bump allocator? Use pool2_ |
83 | | // threshold of 48 bytes. |
84 | | // These only work with GC off -- OILS_GC_THRESHOLD=[big] |
85 | | #ifdef BUMP_SMALL |
86 | | if (num_bytes <= 48) { |
87 | | return gBumpLeak.Allocate(num_bytes); |
88 | | } |
89 | | #endif |
90 | | |
91 | 1.44k | if (to_free_.empty()) { |
92 | | // Use higher object IDs |
93 | 1.28k | *obj_id = greatest_obj_id_; |
94 | 1.28k | greatest_obj_id_++; |
95 | | |
96 | | // This check is ON in release mode |
97 | 1.28k | CHECK(greatest_obj_id_ <= kMaxObjId); |
98 | 1.28k | } else { |
99 | 164 | ObjHeader* dead = to_free_.back(); |
100 | 164 | to_free_.pop_back(); |
101 | | |
102 | 164 | *obj_id = dead->obj_id; // reuse the dead object's ID |
103 | | |
104 | 164 | free(dead); |
105 | 164 | } |
106 | | |
107 | 0 | void* result = malloc(num_bytes); |
108 | 1.44k | DCHECK(result != nullptr); |
109 | | |
110 | 0 | live_objs_.push_back(static_cast<ObjHeader*>(result)); |
111 | | |
112 | 1.44k | num_live_++; |
113 | 1.44k | num_allocated_++; |
114 | 1.44k | bytes_allocated_ += num_bytes; |
115 | | |
116 | 1.44k | return result; |
117 | 6.24k | } |
118 | | |
119 | | #if 0 |
120 | | void* MarkSweepHeap::Reallocate(void* p, size_t num_bytes) { |
121 | | FAIL(kNotImplemented); |
122 | | // This causes a double-free in the GC! |
123 | | // return realloc(p, num_bytes); |
124 | | } |
125 | | #endif |
126 | | |
127 | | // "Leaf" for marking / TraceChildren |
128 | | // |
129 | | // - Abort if nullptr |
130 | | // - Find the header (get rid of this when remove ObjHeader member) |
131 | | // - Tag::{Opaque,FixedSized,Scanned} have their mark bits set |
132 | | // - Tag::{FixedSize,Scanned} are also pushed on the gray stack |
133 | | |
134 | 2.23k | void MarkSweepHeap::MaybeMarkAndPush(RawObject* obj) { |
135 | 2.23k | ObjHeader* header = ObjHeader::FromObject(obj); |
136 | 2.23k | if (header->heap_tag == HeapTag::Global) { // don't mark or push |
137 | 322 | return; |
138 | 322 | } |
139 | | |
140 | 1.91k | int obj_id = header->obj_id; |
141 | 1.91k | #ifndef NO_POOL_ALLOC |
142 | 1.91k | if (header->pool_id == 1) { |
143 | 1.61k | if (pool1_.IsMarked(obj_id)) { |
144 | 145 | return; |
145 | 145 | } |
146 | 1.47k | pool1_.Mark(obj_id); |
147 | 1.47k | } else if (header->pool_id == 2) { |
148 | 276 | if (pool2_.IsMarked(obj_id)) { |
149 | 56 | return; |
150 | 56 | } |
151 | 220 | pool2_.Mark(obj_id); |
152 | 220 | } else |
153 | 19 | #endif |
154 | 19 | { |
155 | 19 | if (mark_set_.IsMarked(obj_id)) { |
156 | 0 | return; |
157 | 0 | } |
158 | 19 | mark_set_.Mark(obj_id); |
159 | 19 | } |
160 | | |
161 | 1.71k | switch (header->heap_tag) { |
162 | 235 | case HeapTag::Opaque: // e.g. strings have no children |
163 | 235 | break; |
164 | | |
165 | 1.30k | case HeapTag::Scanned: // these 2 types have children |
166 | 1.47k | case HeapTag::FixedSize: |
167 | 1.47k | gray_stack_.push_back(header); // Push the header, not the object! |
168 | 1.47k | break; |
169 | | |
170 | 0 | default: |
171 | 0 | FAIL(kShouldNotGetHere); |
172 | 1.71k | } |
173 | 1.71k | } |
174 | | |
175 | 128 | void MarkSweepHeap::TraceChildren() { |
176 | 1.60k | while (!gray_stack_.empty()) { |
177 | 1.47k | ObjHeader* header = gray_stack_.back(); |
178 | 1.47k | gray_stack_.pop_back(); |
179 | | |
180 | 1.47k | switch (header->heap_tag) { |
181 | 171 | case HeapTag::FixedSize: { |
182 | 171 | auto fixed = reinterpret_cast<LayoutFixed*>(header->ObjectAddress()); |
183 | 171 | int mask = FIELD_MASK(*header); |
184 | | |
185 | 4.27k | for (int i = 0; i < kFieldMaskBits; ++i) { |
186 | 4.10k | if (mask & (1 << i)) { |
187 | 181 | RawObject* child = fixed->children_[i]; |
188 | 181 | if (child) { |
189 | 153 | MaybeMarkAndPush(child); |
190 | 153 | } |
191 | 181 | } |
192 | 4.10k | } |
193 | 171 | break; |
194 | 0 | } |
195 | | |
196 | 1.30k | case HeapTag::Scanned: { |
197 | 1.30k | auto slab = reinterpret_cast<Slab<RawObject*>*>(header->ObjectAddress()); |
198 | | |
199 | 1.30k | int n = NUM_POINTERS(*header); |
200 | 3.43k | for (int i = 0; i < n; ++i) { |
201 | 2.13k | RawObject* child = slab->items_[i]; |
202 | 2.13k | if (child) { |
203 | 1.88k | MaybeMarkAndPush(child); |
204 | 1.88k | } |
205 | 2.13k | } |
206 | 1.30k | break; |
207 | 0 | } |
208 | 0 | default: |
209 | | // Only FixedSize and Scanned are pushed |
210 | 0 | FAIL(kShouldNotGetHere); |
211 | 1.47k | } |
212 | 1.47k | } |
213 | 128 | } |
214 | | |
215 | 128 | void MarkSweepHeap::Sweep() { |
216 | 128 | #ifndef NO_POOL_ALLOC |
217 | 128 | pool1_.Sweep(); |
218 | 128 | pool2_.Sweep(); |
219 | 128 | #endif |
220 | | |
221 | 128 | int last_live_index = 0; |
222 | 128 | int num_objs = live_objs_.size(); |
223 | 1.59k | for (int i = 0; i < num_objs; ++i) { |
224 | 1.46k | ObjHeader* obj = live_objs_[i]; |
225 | 1.46k | DCHECK(obj); // malloc() shouldn't have returned nullptr |
226 | | |
227 | 0 | bool is_live = mark_set_.IsMarked(obj->obj_id); |
228 | | |
229 | | // Compact live_objs_ and populate to_free_. Note: doing the reverse could |
230 | | // be more efficient when many objects are dead. |
231 | 1.46k | if (is_live) { |
232 | 19 | live_objs_[last_live_index++] = obj; |
233 | 1.44k | } else { |
234 | 1.44k | to_free_.push_back(obj); |
235 | | // free(obj); |
236 | 1.44k | num_live_--; |
237 | 1.44k | } |
238 | 1.46k | } |
239 | 128 | live_objs_.resize(last_live_index); // remove dangling objects |
240 | | |
241 | 128 | num_collections_++; |
242 | 128 | max_survived_ = std::max(max_survived_, num_live()); |
243 | 128 | } |
244 | | |
245 | 128 | int MarkSweepHeap::Collect() { |
246 | 128 | #ifdef GC_TIMING |
247 | 128 | struct timespec start, end; |
248 | 128 | if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start) < 0) { |
249 | 0 | FAIL("clock_gettime failed"); |
250 | 0 | } |
251 | 0 | #endif |
252 | | |
253 | 0 | int num_roots = roots_.size(); |
254 | 128 | int num_globals = global_roots_.size(); |
255 | | |
256 | 128 | if (gc_verbose_) { |
257 | 0 | log(""); |
258 | 0 | log("%2d. GC with %d roots (%d global) and %d live objects", |
259 | 0 | num_collections_, num_roots + num_globals, num_globals, num_live()); |
260 | 0 | } |
261 | | |
262 | | // Resize it |
263 | 128 | mark_set_.ReInit(greatest_obj_id_); |
264 | 128 | #ifndef NO_POOL_ALLOC |
265 | 128 | pool1_.PrepareForGc(); |
266 | 128 | pool2_.PrepareForGc(); |
267 | 128 | #endif |
268 | | |
269 | | // Mark roots. |
270 | | // Note: It might be nice to get rid of double pointers |
271 | 390 | for (int i = 0; i < num_roots; ++i) { |
272 | 262 | RawObject* root = *(roots_[i]); |
273 | 262 | if (root) { |
274 | 186 | MaybeMarkAndPush(root); |
275 | 186 | } |
276 | 262 | } |
277 | | |
278 | 136 | for (int i = 0; i < num_globals; ++i) { |
279 | 8 | RawObject* root = global_roots_[i]; |
280 | 8 | if (root) { |
281 | 8 | MaybeMarkAndPush(root); |
282 | 8 | } |
283 | 8 | } |
284 | | |
285 | | // Traverse object graph. |
286 | 128 | TraceChildren(); |
287 | | |
288 | 128 | Sweep(); |
289 | | |
290 | 128 | if (gc_verbose_) { |
291 | 0 | log(" %d live after sweep", num_live()); |
292 | 0 | } |
293 | | |
294 | | // We know how many are live. If the number of objects is close to the |
295 | | // threshold (above 75%), then set the threshold to 2 times the number of |
296 | | // live objects. This is an ad hoc policy that removes observed "thrashing" |
297 | | // -- being at 99% of the threshold and doing FUTILE mark and sweep. |
298 | | |
299 | 128 | int water_mark = (gc_threshold_ * 3) / 4; |
300 | 128 | if (num_live() > water_mark) { |
301 | 0 | gc_threshold_ = num_live() * 2; |
302 | 0 | num_growths_++; |
303 | 0 | if (gc_verbose_) { |
304 | 0 | log(" exceeded %d live objects; gc_threshold set to %d", water_mark, |
305 | 0 | gc_threshold_); |
306 | 0 | } |
307 | 0 | } |
308 | | |
309 | 128 | #ifdef GC_TIMING |
310 | 128 | if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end) < 0) { |
311 | 0 | FAIL("clock_gettime failed"); |
312 | 0 | } |
313 | | |
314 | 0 | double start_secs = start.tv_sec + start.tv_nsec / 1e9; |
315 | 128 | double end_secs = end.tv_sec + end.tv_nsec / 1e9; |
316 | 128 | double gc_millis = (end_secs - start_secs) * 1000.0; |
317 | | |
318 | 128 | if (gc_verbose_) { |
319 | 0 | log(" %.1f ms GC", gc_millis); |
320 | 0 | } |
321 | | |
322 | 128 | total_gc_millis_ += gc_millis; |
323 | 128 | if (gc_millis > max_gc_millis_) { |
324 | 66 | max_gc_millis_ = gc_millis; |
325 | 66 | } |
326 | 128 | #endif |
327 | | |
328 | 128 | return num_live(); // for unit tests only |
329 | 128 | } |
330 | | |
331 | 0 | void MarkSweepHeap::PrintShortStats() { |
332 | | // TODO: should use feature detection of dprintf |
333 | 0 | #ifndef OILS_WIN32 |
334 | 0 | #ifndef NO_POOL_ALLOC |
335 | 0 | int fd = 2; |
336 | 0 | dprintf(fd, " num allocated = %10d\n", |
337 | 0 | num_allocated_ + pool1_.num_allocated() + pool2_.num_allocated()); |
338 | 0 | dprintf( |
339 | 0 | fd, "bytes allocated = %10" PRId64 "\n", |
340 | 0 | bytes_allocated_ + pool1_.bytes_allocated() + pool2_.bytes_allocated()); |
341 | 0 | #endif |
342 | 0 | #endif |
343 | 0 | } |
344 | | |
345 | 6 | void MarkSweepHeap::PrintStats(int fd) { |
346 | | // TODO: should use feature detection of dprintf |
347 | 6 | #ifndef OILS_WIN32 |
348 | 6 | dprintf(fd, " num live = %10d\n", num_live()); |
349 | | // max survived_ can be less than num_live(), because leave off the last GC |
350 | 6 | dprintf(fd, " max survived = %10d\n", max_survived_); |
351 | 6 | dprintf(fd, "\n"); |
352 | | |
353 | 6 | #ifndef NO_POOL_ALLOC |
354 | 6 | dprintf(fd, " num allocated = %10d\n", |
355 | 6 | num_allocated_ + pool1_.num_allocated() + pool2_.num_allocated()); |
356 | 6 | dprintf(fd, " num in heap = %10d\n", num_allocated_); |
357 | | #else |
358 | | dprintf(fd, " num allocated = %10d\n", num_allocated_); |
359 | | #endif |
360 | | |
361 | 6 | #ifndef NO_POOL_ALLOC |
362 | 6 | dprintf(fd, " num in pool 1 = %10d\n", pool1_.num_allocated()); |
363 | 6 | dprintf(fd, " num in pool 2 = %10d\n", pool2_.num_allocated()); |
364 | 6 | dprintf( |
365 | 6 | fd, "bytes allocated = %10" PRId64 "\n", |
366 | 6 | bytes_allocated_ + pool1_.bytes_allocated() + pool2_.bytes_allocated()); |
367 | | #else |
368 | | dprintf(fd, "bytes allocated = %10" PRId64 "\n", bytes_allocated_); |
369 | | #endif |
370 | | |
371 | 6 | dprintf(fd, "\n"); |
372 | 6 | dprintf(fd, " num gc points = %10d\n", num_gc_points_); |
373 | 6 | dprintf(fd, " num collections = %10d\n", num_collections_); |
374 | 6 | dprintf(fd, "\n"); |
375 | 6 | dprintf(fd, " gc threshold = %10d\n", gc_threshold_); |
376 | 6 | dprintf(fd, " num growths = %10d\n", num_growths_); |
377 | 6 | dprintf(fd, "\n"); |
378 | 6 | dprintf(fd, " max gc millis = %10.1f\n", max_gc_millis_); |
379 | 6 | dprintf(fd, "total gc millis = %10.1f\n", total_gc_millis_); |
380 | 6 | dprintf(fd, "\n"); |
381 | 6 | dprintf(fd, "roots capacity = %10d\n", |
382 | 6 | static_cast<int>(roots_.capacity())); |
383 | 6 | dprintf(fd, " objs capacity = %10d\n", |
384 | 6 | static_cast<int>(live_objs_.capacity())); |
385 | 6 | #endif |
386 | 6 | } |
387 | | |
388 | | // Cleanup at the end of main() to remain ASAN-safe |
389 | 65 | void MarkSweepHeap::MaybePrintStats() { |
390 | 65 | int stats_fd = -1; |
391 | 65 | char* e = getenv("OILS_GC_STATS"); |
392 | 65 | if (e && strlen(e)) { // env var set and non-empty |
393 | 0 | stats_fd = STDERR_FILENO; |
394 | 65 | } else { |
395 | | // A raw file descriptor lets benchmarks extract stats even if the script |
396 | | // writes to stdout and stderr. Shells can't use open() without potential |
397 | | // conflicts. |
398 | | |
399 | 65 | e = getenv("OILS_GC_STATS_FD"); |
400 | 65 | if (e && strlen(e)) { |
401 | | // Try setting 'stats_fd'. If there's an error, it will be unchanged, and |
402 | | // we don't PrintStats(); |
403 | 0 | StringToInt(e, strlen(e), 10, &stats_fd); |
404 | 0 | } |
405 | 65 | } |
406 | | |
407 | 65 | if (stats_fd != -1) { |
408 | 0 | PrintStats(stats_fd); |
409 | 0 | } |
410 | 65 | } |
411 | | |
412 | 63 | void MarkSweepHeap::FreeEverything() { |
413 | 63 | roots_.clear(); |
414 | 63 | global_roots_.clear(); |
415 | | |
416 | 63 | Collect(); |
417 | | |
418 | | // Collect() told us what to free() |
419 | 1.28k | for (auto obj : to_free_) { |
420 | 1.28k | free(obj); |
421 | 1.28k | } |
422 | 63 | #ifndef NO_POOL_ALLOC |
423 | 63 | pool1_.Free(); |
424 | 63 | pool2_.Free(); |
425 | 63 | #endif |
426 | 63 | } |
427 | | |
428 | 63 | void MarkSweepHeap::CleanProcessExit() { |
429 | 63 | MaybePrintStats(); |
430 | | |
431 | 63 | char* e = getenv("OILS_GC_ON_EXIT"); |
432 | | // collect by default; OILS_GC_ON_EXIT=0 overrides |
433 | 63 | if (e && strcmp(e, "0") == 0) { |
434 | 0 | ; |
435 | 63 | } else { |
436 | 63 | FreeEverything(); |
437 | 63 | } |
438 | 63 | } |
439 | | |
440 | | // for the main binary |
441 | 2 | void MarkSweepHeap::ProcessExit() { |
442 | 2 | MaybePrintStats(); |
443 | | |
444 | | #ifdef CLEAN_PROCESS_EXIT |
445 | | FreeEverything(); |
446 | | #else |
447 | 2 | char* e = getenv("OILS_GC_ON_EXIT"); |
448 | | // don't collect by default; OILS_GC_ON_EXIT=1 overrides |
449 | 2 | if (e && strcmp(e, "1") == 0) { |
450 | 0 | FreeEverything(); |
451 | 0 | } |
452 | 2 | #endif |
453 | 2 | } |
454 | | |
455 | | MarkSweepHeap gHeap; |
456 | | |
457 | | #endif // MARK_SWEEP |