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(), and getTopo()
  • Pure virtual hooks setup() and run() 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:

  1. For every layer, it launches futures on the shared thread_pool_
  2. Elements marked as macros bound to the default thread execute synchronously via macros.emplace_back() to avoid thread-switch overhead
  3. 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:

  1. Validation: Calls GEngine::isDag() to verify acyclicity
  2. Marking: Records front elements (no incoming edges), total elements, and counts end elements (no outgoing edges)
  3. Shape calculation: calcShape() classifies each element as NORMAL, LINKABLE, ROOT, or TAIL
  4. DAG-type analysis: analysisDagType() classifies the graph into one of three internal types defined in GEngineDefine.h:
    • COMMON: Mixed serial/parallel DAG (default)
    • ALL_SERIAL: Pure chain (every element is linkable)
    • ALL_PARALLEL: All elements independent; builds a parallel matrix via analysisParallelMatrix() 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::atomic bookkeeping add cost, making it slower than GStaticEngine for 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 as COMMON, ALL_SERIAL, or ALL_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 GEngine and are interchangeable via GPipeline::setGEngineType() or GElementManager::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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →