diff --git a/xo-gc/include/xo/gc/DX1Collector.hpp b/xo-gc/include/xo/gc/DX1Collector.hpp index c81191d4..fa7dcdd6 100644 --- a/xo-gc/include/xo/gc/DX1Collector.hpp +++ b/xo-gc/include/xo/gc/DX1Collector.hpp @@ -129,6 +129,31 @@ namespace xo { alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; }; + /** @brief statistics from mlog forwarding **/ + class MutationLogStatistics { + public: + MutationLogStatistics() = default; + + MutationLogStatistics & operator+=(const MutationLogStatistics & x) { + n_stale_ += x.n_stale_; + n_live_parent_ += x.n_live_parent_; + n_rescue_ += x.n_rescue_; + n_triage_ += x.n_triage_; + + return *this; + } + + public: + /** count superseded mlog entries **/ + std::size_t n_stale_ = 0; + /** count live parents encountered during mlog scan **/ + std::size_t n_live_parent_ = 0; + /** count child subgraphs rescued during mlog scan **/ + std::size_t n_rescue_ = 0; + /** count triaged mlog entries **/ + std::size_t n_triage_ = 0; + }; + /** @brief info collected during a @ref DX1Collector::verify_ok call * **/ @@ -445,6 +470,44 @@ namespace xo { /** copy roots + everything reachable from them, to to-space **/ void copy_roots(Generation upto) noexcept; + /** cureate new mutation log after copying roots **/ + void forward_mutation_log(Generation upto); + + /** Perform one pass over contents of @p *from_mlog for generation @p gen. + * @p *from_mlog contains all {xgen,xage} pointers that target generation @p gen. + * Surviving mlog entries are moved to either @p *to_mlog or @p *triage_mlog, + * (generation < @p upto being collected this cycle). + * + * Each mlog entry gets one of the following outcomes. + * 1. skip. mlog entry has been superseded by another mut at target site. + * 2. keep. mlog entry is live. destination has been evacuated, + * so source must be updated as well. + * 3. triage. source of incoming object belongs to a generation that was collected, + * and has not been evacuated. Although appears to be garbage, it may + * be live after all if reachable from the destination of some other + * mlog entry in @p *to_mlog. Store these mlog entries in @p *triage_mlog. + * + * @return number of mlog entries moved, whether to @p *to_mlog or @p *triage_mlog. + **/ + MutationLogStatistics _forward_mutation_log_phase(Generation upto, + Generation gen, + MutationLog * from_mlog, + MutationLog * to_mlog, + MutationLog * triage_mlog); + + MutationLogStatistics _preserve_child_of_live_parent(Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog); + + /** helper function to decide whether to keep a mutation log entry + * @return true iff mlog entry appended to @p keep_mlog + **/ + bool _check_keep_mutation_aux(const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog); + /** cleanup after gc **/ void cleanup_phase(Generation upto); diff --git a/xo-gc/include/xo/gc/MutationLogEntry.hpp b/xo-gc/include/xo/gc/MutationLogEntry.hpp index eca086d6..c76427a3 100644 --- a/xo-gc/include/xo/gc/MutationLogEntry.hpp +++ b/xo-gc/include/xo/gc/MutationLogEntry.hpp @@ -28,12 +28,16 @@ namespace xo { using AGCObject = xo::mm::AGCObject; public: + MutationLogEntry() = default; MutationLogEntry(void * parent, void ** p_data, obj snap); void * parent() const { return parent_; } void ** p_data() const { return p_data_; } obj snap() const { return snap_; } + /** true if child pointer has been altered since this mlog entry created **/ + bool is_superseded() const noexcept { return *p_data_ != snap_.data(); } + private: /** address of object containing logged mutation **/ void * parent_ = nullptr; @@ -41,7 +45,7 @@ namespace xo { * driving this log entry. **/ void ** p_data_ = nullptr; - /** AGCObject i/face pointer, asof assignment responsible for this log entry. + /** AGCObject child pointer, asof assignment responsible for this log entry. * If *p_data_ matches snap_.data(), then AGCObject interface is snap_.iface(). * Otherwise log entry has been superseded by another assignment. **/ diff --git a/xo-gc/include/xo/gc/X1CollectorConfig.hpp b/xo-gc/include/xo/gc/X1CollectorConfig.hpp index 83f03715..155a1e5d 100644 --- a/xo-gc/include/xo/gc/X1CollectorConfig.hpp +++ b/xo-gc/include/xo/gc/X1CollectorConfig.hpp @@ -40,6 +40,15 @@ namespace xo { return Generation(age % n_survive_threshold_); } + /** age threshold for promotion to generation @p g **/ + uint32_t promotion_threshold(Generation g) const noexcept { + // TODO: may consider replacing with table-lookup + // Require: if two distinct ages promote to some gen g at the same time, + // then they also promote to gen g+k at the same time for all k>0. + + return g * n_survive_threshold_; + } + public: // ----- Instance Variables ----- diff --git a/xo-gc/src/gc/DX1Collector.cpp b/xo-gc/src/gc/DX1Collector.cpp index 36082453..9ad19487 100644 --- a/xo-gc/src/gc/DX1Collector.cpp +++ b/xo-gc/src/gc/DX1Collector.cpp @@ -949,14 +949,18 @@ namespace xo { this->copy_roots(upto); log && log("step 2b : [STUB] copy pinned"); - log && log("step 3a : [STUB] run destructors"); - log && log("step 3b : [STUB] keep reachable weak pointers"); - log && log("step 4 : cleanup"); + log && log("step 3 : [STUB] forward mutation log"); + this->forward_mutation_log(upto); + + log && log("step 4a : [STUB] run destructors"); + log && log("step 4b : [STUB] keep reachable weak pointers"); + + log && log("step 5 : cleanup"); this->cleanup_phase(upto); if (config_.sanitize_flag_) { - log && log("step 4b : verify"); + log && log("step 5b : verify"); bool ok = this->verify_ok(); log && log(xtag("n-gc-root", verify_stats_.n_gc_root_), @@ -983,6 +987,282 @@ namespace xo { log && log("swap roles", xtag("g", g)); std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); + std::swap(mlog_[role::to_space()][g], mlog_[role::from_space()][g]); + } + } + + void + DX1Collector::forward_mutation_log(Generation upto) + { + /** non-zero if at least one object was rescued (from any generation) + * by mutation log scan + **/ + std::size_t work = 0; + + do { + // on 1st iteration, for all generations: + // - to_mlog, triage_mlog are empty + + for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { + + MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; + + if (!from_mlog->empty()) { + MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + auto stats = this->_forward_mutation_log_phase(upto, + child_gen, + from_mlog, + to_mlog, + triage_mlog); + + from_mlog->clear(); + + // {from_mlog, triage_mlog} reverse roles + + std::swap(this->mlog_[role::from_space()][child_gen], + this->mlog_[c_n_role][child_gen]); + + work += stats.n_rescue_; + } + } + } while (work > 0); + + // here: reached fixpoints, any remaining triaged mlogs can be discarded + for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + triage_mlog->clear(); + } + } + + MutationLogStatistics + DX1Collector::_forward_mutation_log_phase(Generation upto, + Generation child_gen, + MutationLog * from_mlog, + MutationLog * keep_mlog, + MutationLog * triage_mlog) + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("child_gen", child_gen), + xtag("mlog.size", from_mlog->size())); + + /* categorize each mlog entry based on combination of {src, dest}. + * In each case we care about {gen, age} of {src, dest} + * objects. + * + * Enough cases to deserve a table: + * + * Legend: + * - P : parent object + * - P' : parent object after this gc phase + * - g(P) : generation of parent P. + * '+' if gen > child_gen (parent gen not collected this cycle) + * - age(P) : age of parent P. + * + * - C : child object + * - C' : child object after this gc phase + * - g(C) : generation of child C. + * - age(C) : age of child C. + * + * - 0 : *from_mlog targets this object's generation. + * object not eligible for promotion. + * Write self* for objects eligible that promote + * if they survive this gc cycle. + * Write + for 'any generation senior to target' + * - 1 : *from_mlog target this object's generation; + * object promotes if it survives + * + * - role : 'to' this phase evacuated + * (or in generation not eligible for collection) + * 'fr' otherwise + * + * | mlog | par | | | mlog | upd + * case | cur | g(P) | P C | C' | action | P | move + * -------+------+-------+--------------+------+---------+--------+----- + * MLOG0 | no | | | | discard | | - + * | | | | | | | + * MLOG1 | yes | * | to:+ fwd:* | to | keep | P->C' | - + * MLOG2 | yes | | fr:0 | to | keep | P->C' | C->to + * MLOG3 | yes | * | fwd:* - | to | update | P'->C' | - + * MLOG4 | yes | | fr:* - | - | triage | - | - + + * notes: + * MLOG1 : child C already forwarded (whether or not promoted) + * MLOG2 : child C survives (and perhaps promoted). + * kept alive by parent in more-senior generation + * MLOG3 : parent has been forwarded. + * update mlog entry for new parent location + * MLOG4 : parent provisionally garbage. triage mlog entry until + * definite outcome. + */ + + MutationLogStatistics counters; + // index of current mlog entry during evac + std::uint32_t i_from = 0; + + for (MutationLogEntry & from_entry : *from_mlog) { + if (log) { + log(xtag("i_from", i_from)); + } + + if (from_entry.is_superseded()) { + // there must be a second mlog entry that refers to + // the new child. Rely on that second entry, + // skipping this one. + + // [MLOG0] obsolete mutation -> skip + ++counters.n_stale_; + continue; + } + + /* here: mlog current */ + + Generation parent_gen_to = this->generation_of(role::to_space(), + from_entry.parent()); + + if (parent_gen_to.is_sentinel()) { + void * parent_fr = *from_entry.p_data(); + + AllocInfo parent_info = this->alloc_info((std::byte *)parent_fr); + + if (parent_info.is_forwarding_tseq()) { + /* [MLOG3] */ + + ++counters.n_live_parent_; + + // new parent location in to-space + // TODO: method on AllocInfo to streamline this + void * parent_to = *(void **)parent_fr; + + parent_gen_to = this->generation_of(role::to_space(), + parent_to); + parent_info = this->alloc_info((std::byte *)parent_to); + + assert(!parent_gen_to.sentinel()); + + // Since parent already forwarded, we don't have to preserve child + // or update parent object. + // + // Do need to replace mlog entry to reflect new parent location. + + std::size_t offset + = ((std::byte *)from_entry.p_data() + - (std::byte *)from_entry.parent()); + + void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); + void * child_to = *p_data_to; + + MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); + + this->_check_keep_mutation_aux(to_entry, + parent_gen_to, + child_to, + keep_mlog); + + + } else { + ++counters.n_triage_; + + // parent hasn't been collected and may be garbage. + // However this is only provisional, since + // parent could turn out to be reachable via some other mutation. + + triage_mlog->push_back(from_entry); + } + } else { + /* [MLOG1, MLOG2] */ + + counters += this->_preserve_child_of_live_parent(upto, + parent_gen_to, + from_entry, + keep_mlog); + } + } + + return counters; + } + + MutationLogStatistics + DX1Collector::_preserve_child_of_live_parent(Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog) + { + void * child_fr = *from_entry.p_data(); + AllocInfo child_info = this->alloc_info((std::byte *)(child_fr)); + + MutationLogStatistics counters; + + // if child collected: new child location in to-space + void * child_to = nullptr; + + // parent is alive: gc must ensure child remains alive + + ++counters.n_live_parent_; + + // Parent already recognized as alive. Either not subject to collection + // or already evacuated. + // (+ remember this need not be 1st pass over mlog entries) + + if (child_info.is_forwarding_tseq()) { + // [MLOG1] + + // child already forwarded. + // TODO: make this a method on AllocInfo + child_to = *(void **)child_fr; + + // assigning through address of P->C pointer + // also makes mlog entry current + + } else { + // [MLOG2] + + ++counters.n_rescue_; + + child_to = this->_deep_move_interior(child_fr, upto); + + // update child pointer in parent object + *from_entry.p_data() = child_to; + } + + // child_to generation in {gen, gen+1} + + this->_check_keep_mutation_aux(from_entry, parent_gen, child_to, keep_mlog); + + return counters; + } + + bool + DX1Collector::_check_keep_mutation_aux(const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog) + { + Generation child_gen_to + = this->generation_of(role::to_space(), child_to); + + bool need_mlog_entry + = ((child_gen_to + 1 < config_.n_generation_) + && (config_.promotion_threshold(parent_gen_to) + > config_.promotion_threshold(child_gen_to))); + + if (need_mlog_entry) { + // 1. P->C pointer is still cross-age (xage), and + // 2. this matters; in future P will promote before C + // + // Need to keep entry because parent will be eligible for promotion + // before child + + keep_mlog->push_back(from_entry); + + return true; + } else { + // child now in final generation, + // no longer need to track incoming mutations. + + return false; } }