# GStaticEngine vs GDynamicEngine: How Execution Strategy Impacts CGraph Performance

> Discover how GStaticEngine and GDynamicEngine impact CGraph performance. Learn about static DAG execution matrices versus dynamic adaptive scheduling to optimize your graph processing.

- Repository: [Chunel/cgraph](https://github.com/chunelfeng/cgraph)
- Tags: performance
- Published: 2026-02-27

---

**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`](https://github.com/chunelfeng/cgraph/blob/main/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`](https://github.com/chunelfeng/cgraph/blob/main/GEngineDefine.h)), with values `STATIC` and `DYNAMIC`. Users configure this through `GPipeline::setGEngineType()` or `GElementManager::setEngineType()`, located in [`src/GraphCtrl/GraphPipeline/GPipeline.h`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphPipeline/GPipeline.h) and [`src/GraphCtrl/GraphElement/GElementManager.h`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphElement/GElementManager.h) respectively.

## GStaticEngine: Pre-Computed Static Scheduling

`GStaticEngine` (implemented in [`src/GraphCtrl/GraphElement/_GEngine/GStaticEngine/GStaticEngine.h`](https://github.com/chunelfeng/cgraph/blob/main/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.

```cpp
// 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`](https://github.com/chunelfeng/cgraph/blob/main/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`](https://github.com/chunelfeng/cgraph/blob/main/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

```cpp
#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

```cpp
#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

```cpp
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`](https://github.com/chunelfeng/cgraph/blob/main/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.