How to Tune DART Simulation Performance: Optimization Best Practices for dartsim/dart
The most effective way to tune DART simulation performance is to profile the dominant computational layer—collision detection, dynamics integration, or LCP constraint solving—and apply targeted optimizations such as selecting faster iterative solvers, enabling SIMD builds, and splitting independent constraint groups.
Tuning DART simulation performance requires understanding its layered architecture that separates foundation math, collision detection, rigid body dynamics, and constraint solving. According to the dartsim/dart source code, each layer has distinct computational complexity characteristics that dictate where optimization efforts yield the greatest speedup.
Understanding DART's Computational Architecture
DART's performance is determined by five distinct layers, each with specific complexity profiles documented in docs/onboarding/architecture.md【L81-L95】:
| Layer | Computational Complexity | Primary Cost Driver |
|---|---|---|
| Foundation (math utilities) | O(n) | Vector operations, SIMD utilization |
| Collision Detection | O(m log m) broad-phase, O(p) narrow-phase | Backend choice, pair filtering |
| Dynamics (ABA algorithm) | O(n) | Tree depth, joint degrees of freedom |
| Constraints (LCP solve) | O(c³) | Number of active constraints |
| Integration | O(n) | Time-step size, semi-implicit Euler stability |
The Constraint layer typically dominates runtime in contact-rich simulations due to its cubic complexity, while the Dynamics layer scales linearly with degrees of freedom. Targeting the correct layer based on your scenario is essential before applying any performance knobs.
Essential Tuning Strategies
Time Step and Integration Settings
Keep the time step (dt) modest—the default is 0.001 seconds (1 kHz). Larger time steps force the semi-implicit Euler integrator to work harder to maintain stability, often requiring additional constraint iterations that compound the O(c³) cost【L104-L106】.
Disable unnecessary sensors and trajectory recorders if real-time visualization is not required. Each additional sensor adds per-step overhead that bypasses the core physics optimizations.
Collision Detection Optimization
Use the FCL backend (default) with mesh mode enabled for accurate but fast contact generation【L60-L66】. Enable broad-phase filtering via CollisionFilter to prune collision pairs that cannot interact, reducing narrow-phase checks from O(m²) to O(m log m)【L54-L55】.
For static environments, pre-compute Bounding Volume Hierarchies (BVH) and reuse them across frames to minimize redundant narrow-phase calculations.
Constraint Solver Configuration
The LCP (Linear Complementarity Problem) solver choice significantly impacts performance. The default DantzigSolver provides robust convergence but executes slower iterative steps. For scenarios with many contacts, switch to the PgsSolver (Projected Gauss-Seidel), which offers faster convergence at the cost of reduced accuracy【L41-L43】.
Split independent constraint systems using ConstrainedGroup objects. DART solves each group separately, effectively reducing the c variable in the O(c³) complexity calculation【L70-L73】. Disable soft-contact constraints and unnecessary joint limit enforcement when not required.
SIMD and Memory Optimization
Build DART with SIMD enabled targeting your hardware's instruction set (AVX2, AVX-512, or ARM NEON). Ensure Eigen is compiled with matching flags to maximize vectorized throughput in the foundation math layer【L45-L53】.
Use pool allocators instead of heap allocation for per-step temporary memory. DART implements this pattern in dart/common/PoolAllocator.hpp—custom simulation code should allocate from PoolAllocator or FrameAllocator to avoid heap churn【L76-L81】.
Enable lazy evaluation and version tracking for mass-matrix caching. DART only recomputes the mass matrix when state changes, avoiding repeated O(n²) builds during static contact phases【L98-L104】.
Implementing Performance Tuning in Code
Selecting a Faster LCP Solver
Replace the default Dantzig solver with PGS for speed-critical applications:
#include <dart/dart.hpp>
int main() {
auto world = dart::simulation::World::create();
world->setTimeStep(0.001);
world->setGravity(Eigen::Vector3d(0, 0, -9.81));
// Load robot and add to world
auto robot = dart::io::loadSkeleton("robot.urdf");
world->addSkeleton(robot);
// Switch to Projected Gauss-Seidel for faster constraint solving
world->getConstraintSolver()->setLcpSolver(
dart::math::PgsSolver::create());
for (int i = 0; i < 1000; ++i) {
world->step();
}
}
This change targets the O(c³) constraint layer bottleneck identified in docs/onboarding/architecture.md【L41-L44】.
Enabling SIMD and Pool Allocation
Configure the build with native SIMD flags:
pixi run build -- -DCMAKE_CXX_FLAGS="-march=native -msse4.2 -mavx2"
Utilize pool allocators in custom simulation loops to eliminate heap fragmentation:
#include <dart/common/PoolAllocator.hpp>
#include <dart/simd/Vector.hpp>
void simulateStep(dart::simulation::WorldPtr world) {
dart::common::PoolAllocator pool;
// Allocate SIMD vectors from pool (zero heap allocation)
auto temp = pool.allocate<dart::simd::Vector<double, 4>>();
temp = world->getSkeleton(0)->getVelocities().head<4>();
// Process with SIMD-backed math
auto result = dart::simd::transformPoint(temp, Eigen::Isometry3d::Identity());
world->step();
}
The SIMD module is defined in dart/simd/All.hpp and requires matching Eigen compilation flags【L45-L53】【L76-L81】.
Optimizing Constraint Groups
Force parallel solving of independent constraint groups:
auto solver = world->getConstraintSolver();
// Recompute groups after adding bodies or changing topology
solver->clearConstrainedGroups();
solver->generateConstrainedGroups();
// Enable parallel solving across CPU cores
solver->setParallel(true);
This optimization is referenced in the constraint layer documentation【L70-L73】.
Advanced Performance Techniques
Joint Type Selection: Prefer revolute or prismatic joints over high-DOF joints like FreeJoint when possible. Fewer degrees of freedom directly reduce the O(n) cost of the Articulated Body Algorithm (ABA) dynamics pass.
Contact Softening: Apply ERP (Error Reduction Parameter) and CFM (Constraint Force Mixing) to relax stiff contacts. Softer constraints converge faster in the LCP solver, allowing larger time steps without stability loss.
Memory Alignment: Ensure data structures align to cache-line sizes (64 bytes on x86_64). DART handles this automatically via Eigen::aligned_allocator, but custom data structures should follow suit to prevent SIMD throughput degradation.
Profiling Workflow: Run the built-in SIMD benchmarks with pixi run bm-simd and analyze LCP performance using scripts/lcp_performance_profile.py to identify actual bottlenecks before optimizing【L119-L124】.
Summary
- Profile first using DART's built-in benchmarks (
bm-simd,lcp_performance_profile.py) to identify whether collision, dynamics, or constraints dominate your simulation. - Target the O(c³) constraint layer by switching to
PgsSolver, splitting independent groups withConstrainedGroup, and disabling unnecessary constraints. - Optimize collision with FCL backend broad-phase filtering and BVH caching for static meshes.
- Enable SIMD builds with matching Eigen flags and use
PoolAllocatorto eliminate per-step heap allocations. - Maintain modest time steps (0.001s default) to keep the semi-implicit Euler integrator stable without excessive constraint iterations.
Frequently Asked Questions
How do I know which DART layer is slowing down my simulation?
Run the profiling scripts included in the repository: pixi run bm-simd benchmarks vectorized math operations, while scripts/lcp_performance_profile.py analyzes constraint solver performance. If collision detection dominates, optimize broad-phase filtering; if LCP solving consumes time, switch to PgsSolver or reduce constraint counts.
Is the DantzigSolver or PgsSolver better for real-time applications?
PgsSolver (Projected Gauss-Seidel) is better for real-time applications requiring high frame rates. While DantzigSolver offers superior accuracy for stiff constraints and complex contact scenarios, it executes slower iterative steps that may exceed time budgets in O(c³) systems with many contacts.
Does DART automatically use multiple CPU cores?
DART supports parallel constraint solving when explicitly enabled. Call ConstraintSolver::setParallel(true) after generating constrained groups with generateConstrainedGroups(). Independent groups are then solved across available cores, scaling performance with CPU count for systems with separable constraint islands.
What time step should I use for stable DART simulations?
The default 0.001 seconds (1 kHz) provides a good balance between stability and performance. Larger steps risk integration error accumulation in the semi-implicit Euler integrator, potentially requiring additional constraint iterations that compound the cubic cost of LCP solving. Reduce the step size if you observe instability in stiff systems.
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 →