GStaticEngine vs GDynamicEngine: How Execution Strategy Impacts CGraph Performance
GStaticEngine pre-computes a fixed execution matrix for static DAGs, while GDynamicEngine adapts to runtime topology changes with affinity-based scheduling, trading setup overhead for flexibility.
CGraph is a high-performance C++ DAG (Directed Acyclic Graph) execution framework that abstracts graph scheduling behind the GEngine interface. The choice between GStaticEngine and GDynamicEngine directly determines how the library builds execution plans, manages thread pools, and handles runtime graph mutations. Understanding the impact of GStaticEngine vs GDynamicEngine on CGraph execution helps developers optimize for either predictable throughput or adaptive flexibility.
Architecture Overview
Both engines inherit from the abstract base class defined in src/GraphCtrl/GraphElement/_GEngine/GEngine.h. This base provides:
- A shared thread-pool for parallel execution
- Topological utilities:
calcShape(),isDag(), andgetTopo() - Pure virtual hooks
setup()andrun()that concrete engines must implement
The engine type is controlled via GEngineType enum (defined in GEngineDefine.h), with values STATIC and DYNAMIC. Users configure this through GPipeline::setGEngineType() or GElementManager::setEngineType(), located in src/GraphCtrl/GraphPipeline/GPipeline.h and src/GraphCtrl/GraphElement/GElementManager.h respectively.
GStaticEngine: Pre-Computed Static Scheduling
GStaticEngine (implemented in src/GraphCtrl/GraphElement/_GEngine/GStaticEngine/GStaticEngine.h and .cpp) is optimized for DAGs with fixed topologies that do not change between executions.
Setup Phase
During GStaticEngine::setup(), the engine performs a one-time topological traversal to build element_mat_—a 2-D matrix where each inner vector represents a topological layer that can execute in parallel.
// Conceptual representation from GStaticEngine.cpp
element_mat_[0] = {layer_0_elements}; // Root nodes
element_mat_[1] = {layer_1_elements}; // Dependent on layer 0
// ... continues until all layers are mapped
The setup validates that the total visited nodes equals the original set size (totalSize != elements.size() signals a parse error), ensuring the DAG is fully covered.
Execution Phase
GStaticEngine::run() executes layers sequentially while parallelizing within each layer:
- For every layer, it launches futures on the shared
thread_pool_ - Elements marked as macros bound to the default thread execute synchronously via
macros.emplace_back()to avoid thread-switch overhead - After all futures in a layer complete, the engine proceeds to the next layer, enforcing layer-wise synchronization
Performance Characteristics
- Predictable memory layout: The entire schedule is known upfront, minimizing per-iteration overhead
- Best suited for static DAGs: Ideal for high-frequency inference pipelines where graph structure never changes
- Limited adaptivity: Any structural change (new dependencies, dynamic branching) forces a full rebuild of
element_mat_, which can be costly
GDynamicEngine: Adaptive Runtime Scheduling
GDynamicEngine (implemented in src/GraphCtrl/GraphElement/_GEngine/GDynamicEngine/GDynamicEngine.h and .cpp) adapts to runtime topology changes and automatically selects execution strategies based on DAG shape analysis.
Setup Phase
GDynamicEngine::setup() performs comprehensive DAG analysis:
- Validation: Calls
GEngine::isDag()to verify acyclicity - Marking: Records front elements (no incoming edges), total elements, and counts end elements (no outgoing edges)
- Shape calculation:
calcShape()classifies each element asNORMAL,LINKABLE,ROOT, orTAIL - DAG-type analysis:
analysisDagType()classifies the graph into one of three internal types defined inGEngineDefine.h:COMMON: Mixed serial/parallel DAG (default)ALL_SERIAL: Pure chain (every element is linkable)ALL_PARALLEL: All elements independent; builds a parallel matrix viaanalysisParallelMatrix()grouped by thread-pool configuration
Execution Strategies
GDynamicEngine::run() dispatches to specialized paths based on dag_type_:
| Dag Type | Execution Path | Description |
|---|---|---|
COMMON |
commonRunAll() |
Starts with front elements, recursively schedules successors using affinity (same thread when possible) via process() |
ALL_SERIAL |
serialRunAll() |
Linear, single-threaded walk through the chain |
ALL_PARALLEL |
parallelRunAll() |
Either micro-batch (future-based) or task-per-thread mode using thread_pool_->executeWithTid and per-engine parallel_run_num_ counter |
Affinity and Parallelism
The core scheduling primitive is process(), which decides whether to run an element directly (affinity) or hand it to the thread-pool. After completion, afterElementRun() examines the element's shape to schedule successors—keeping one successor "affine" for cache-friendly execution.
Performance Characteristics
- Flexibility: Handles any DAG shape without rebuilding matrices; supports runtime graph mutations
- Fine-grained parallelism: Per-element affinity and reserved successor threads reduce cache thrashing in deep pipelines
- Higher runtime overhead: Dynamic analysis and
std::atomicbookkeeping add cost, making it slower thanGStaticEnginefor small, static graphs
Choosing Between Static and Dynamic Engines
Select GStaticEngine when:
- The graph topology is fixed at compile-time and never changes between runs
- Execution latency is critical (e.g., high-frequency trading, real-time inference)
- You want minimal per-run scheduling overhead
Select GDynamicEngine when:
- The graph structure may change at runtime (conditional branches, dynamic data-flow)
- You need automatic optimization selection (serial vs. parallel) based on actual DAG shape
- Cache affinity and thread affinity are important for deep, complex pipelines
Both engines expose identical APIs through GElementManager::setEngineType() and GPipeline::setGEngineType(), allowing one-line configuration changes without modifying pipeline logic.
Configuration Examples
Using the Static Engine
#include "CGraph.h"
int main() {
CGRAPH_NAMESPACE_BEGIN
// Create a pipeline and force static execution
GPipeline pipeline;
pipeline.setGEngineType(GEngineType::STATIC); // Static engine selection
// Add elements (example: a simple two-step pipeline)
auto *a = pipeline.addElement<GElement>("A");
auto *b = pipeline.addElement<GElement>("B");
a->runBefore(b); // Define dependency: A → B
pipeline.run(); // Static engine builds the matrix once and runs
CGRAPH_NAMESPACE_END
}
The static engine builds element_mat_ with two layers ({A}, {B}) and executes them in strict topological order.
Using the Dynamic Engine
#include "CGraph.h"
int main() {
CGRAPH_NAMESPACE_BEGIN
GPipeline pipeline;
pipeline.setGEngineType(GEngineType::DYNAMIC); // Dynamic engine selection
// Build a more complex DAG
auto *src = pipeline.addElement<GElement>("src");
auto *mid1 = pipeline.addElement<GElement>("mid1");
auto *mid2 = pipeline.addElement<GElement>("mid2");
auto *sink = pipeline.addElement<GElement>("sink");
src->runBefore(mid1);
src->runBefore(mid2);
mid1->runBefore(sink);
mid2->runBefore(sink);
pipeline.run(); // Dynamic engine analyses the DAG and picks COMMON path
CGRAPH_NAMESPACE_END
}
The dynamic engine detects a mixed DAG (COMMON type) and starts src, then schedules mid1 and mid2 in parallel, finally executing sink.
Default Automatic Selection
GPipeline pipeline; // Defaults to GEngineType::DYNAMIC
// ... add elements as needed ...
pipeline.run(); // The engine will analyze the graph and select the optimal path
Summary
-
GStaticEngine (
src/GraphCtrl/GraphElement/_GEngine/GStaticEngine/) pre-computes a 2-D execution matrix (element_mat_) during setup, enabling layer-wise parallel execution with minimal runtime overhead. Best for fixed-topology DAGs where low latency is critical. -
GDynamicEngine (
src/GraphCtrl/GraphElement/_GEngine/GDynamicEngine/) performs runtime DAG analysis (analysisDagType) to classify graphs asCOMMON,ALL_SERIAL, orALL_PARALLEL, then dispatches to specialized execution paths with affinity-based scheduling. Ideal for dynamic graphs and complex topologies requiring cache optimization. -
Both engines inherit from
GEngineand are interchangeable viaGPipeline::setGEngineType()orGElementManager::setEngineType(), allowing optimization without code changes.
Frequently Asked Questions
What is the main performance difference between GStaticEngine and GDynamicEngine?
GStaticEngine offers lower per-execution overhead because it builds the execution matrix (element_mat_) once during setup and reuses it for every run. GDynamicEngine incurs higher runtime cost due to DAG type analysis and atomic bookkeeping, but provides better performance for dynamic topologies through affinity-based scheduling and automatic parallelization strategies.
When should I use GStaticEngine over GDynamicEngine?
Use GStaticEngine when your DAG topology is fixed at compile-time and never changes between executions, such as in high-frequency inference pipelines or real-time trading systems where minimal scheduling latency is critical. The static engine is also preferable for small, simple graphs where the dynamic engine's analysis overhead would outweigh its benefits.
How does GDynamicEngine handle different DAG shapes?
GDynamicEngine classifies DAGs into three internal types during setup: ALL_SERIAL for pure chains (executed via serialRunAll()), ALL_PARALLEL for independent nodes (executed via parallelRunAll() with task-per-thread or micro-batch modes), and COMMON for mixed topologies (executed via commonRunAll() with affinity-based scheduling). This automatic classification allows the engine to select the most efficient execution strategy without manual configuration.
Can I switch between engines without changing my pipeline code?
Yes. Both engines implement the same abstract interface defined in GEngine.h. You can switch between GStaticEngine and GDynamicEngine by calling pipeline.setGEngineType(GEngineType::STATIC) or pipeline.setGEngineType(GEngineType::DYNAMIC) without modifying any element definitions or dependency logic. The engine selection is transparent to the graph structure and element implementations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →