Health-GPS

Logo

Global Health Policy Simulation model

View the Project on GitHub imperialCHEPI/healthgps

Parallelize output writes and reduce is_active() calls

Global Health Policy Simulation model

Home Quick Start User Guide Schemas Models Architecture Data Model Developer Guide Technical docs API
Related: Individual ID tracking plan Technical index Documentation index

Plan summary

Title: Parallelize output writes and is_active

Overview: Parallelize result/tracking file writes by using two writer threads (main result vs individual tracking) and a second queue, then reduce redundant is_active() calls in hot paths (e.g. analysis module) by caching or filtering once per population iteration.

Current behaviour


Phase 1: Parallelize output writing

Goal: Let main result writes and individual-tracking writes run on different threads so they can proceed in parallel (different files, no shared state).

Approach: two queues, two dispatch threads.

  1. Add a second queue and thread in EventMonitor (event_monitor.h, event_monitor.cpp):
  1. Route messages by type:
  1. Start and stop both threads:
  1. Optional (later): parallelize inside ResultFileWriter::write()

Result: Main result file(s) and individual-tracking file are written by two threads in parallel; ordering within each file is unchanged.


Phase 2: Reduce number of is_active() calls

Goal: Avoid calling is_active() repeatedly on the same person in the same logical “iteration” (e.g. same year, same module).

Where it’s used (examples):

Options:

  1. Cache per iteration in analysis module: For a single “year” or “publish” pass, build once a compact structure (e.g. std::vector<bool> or bit set) of “active” by index, then in subsequent loops over the same population snapshot use that cache instead of calling entity.is_active(). Downside: population can change during a year (deaths, births); so the cache is only valid if all uses in that pass see the same snapshot. In publish_result_message and the various calculate_* paths, the population is not modified during the same call, so a cache per function scope is valid.
  2. Filter once into an “active” index list: One loop over the population that builds std::vector<std::size_t> active_indices, then later loops iterate for (auto i : active_indices) { auto& entity = population[i]; ... }. This replaces many is_active() checks with one pass and then direct iteration. Same caveat: use within a single logical pass where population is not changing.
  3. Leave hot loops in disease/risk-factor modules as-is initially: Focus on analysis_module where a single “result” computation does multiple full-population passes; there caching or a single “active” index list gives the biggest benefit.

Recommendation: Start with analysis_module only. In functions that do multiple passes over context.population() in one go (e.g. calculate_historical_statistics, or the block that does DALYs + risk-factor sums + comorbidity + prevalence), add one initial pass that fills std::vector<bool> is_active(pop.size()) (or an index list), then use that in subsequent loops instead of calling entity.is_active(). Measure before/after if needed.

Scope for plan: Implement Phase 1 (two writer threads) first; then implement Phase 2 in analysis_module (one or two key functions) and leave a short comment for extending to other modules later.


File change summary (Phase 1)

File Change
event_monitor.h Add tracking_results_queue_, declare tracking_dispatch_thread(), and second tg_.run() for it.
event_monitor.cpp individual_tracking subscriber pushes to tracking_results_queue_; implement tracking_dispatch_thread() (same loop as result_dispatch_thread but pop from tracking queue); start tracking thread in ctor.

No changes to result_file_writer.cpp or individual_id_tracking_writer.cpp for Phase 1; each writer stays single-threaded from its own dispatch thread.


File change summary (Phase 2)

File Change
analysis_module.cpp In one or two hot functions that do multiple population passes (e.g. calculate_historical_statistics), add a single initial pass that builds a cache of active flags (or active indices), then use that cache in later loops instead of calling person.is_active().

Order of implementation

  1. Phase 1: EventMonitor two-queue, two-thread parallel writes.
  2. Phase 2: In analysis_module, add is_active cache (or active index list) in the heaviest multi-pass function and replace repeated is_active() checks with the cache.

Author: Mahima Ghosh