6.0 KiB
Benchmarks
Performance claims require measurement. QuanTAlib is benchmarked against established libraries: TA-Lib and Tulip (industry-standard C libraries accessed via P/Invoke), Skender.Stock.Indicators and Ooples.FinancialIndicators (popular .NET implementations).
Test Environment
- Data: 500,000 bars
- Parameters: Period 220 (sufficient scale to expose algorithmic inefficiencies)
- Framework: NET 10.0.0 (10.0.25.52411), X64 AOT AVX-512F+CD+BW+DQ+VL+VBMI
- Hardware: AMD Ryzen 9 9950X 16-Core Processor (4.30 GHz) supporting AVX-512 SIMD and FMA (Fused Multiply-Add)
These results represent what current-generation server CPUs achieve in production.
SIMD (Single Instruction, Multiple Data) and FMA (Fused Multiply-Add)
The library automatically detects and utilizes the highest available instruction set (AVX-512, AVX2, or NEON). This allows processing multiple data points simultaneously:
- AVX-512: Processes 8
doublevalues per cycle (512-bit vectors). - AVX2: Processes 4
doublevalues per cycle (256-bit vectors). - NEON: Processes 2
doublevalues per cycle (128-bit vectors).
QuanTAlib also leverages Fused Multiply-Add (FMA) instructions (FMA3) wherever possible - for scalar and vector math. FMA performs a multiplication and addition in a single CPU cycle (a * b + c) with a single rounding step. This provides two distinct advantages:
- Throughput: Doubling the floating-point operations per cycle compared to separate multiply and add instructions.
- Precision: Reducing cumulative rounding errors in iterative calculations like moving averages and standard deviations.
In algorithms heavily reliant on convolution or dot products (like WMA, LinReg, or Correlation), SIMD and FMA usage contributes significantly to the observed speedup over traditional implementations.
Benchmark Results
Simple Moving Average (SMA)
QuanTAlib's Span mode calculates 500,000 SMA values in 319 microseconds with zero memory allocations. That's 0.64 nanoseconds per value. For context, a single L1 cache access takes approximately 1 nanosecond on modern CPUs, so moving averages are being calculated faster than data can be fetched from the nearest cache level.
| Library | Mean Time | Allocations | Relative Speed |
|---|---|---|---|
| QuanTAlib (Span) | 318.5 μs | 0 B | 1.00x (baseline) |
| TA-Lib | 370.7 μs | 34 B | 1.16x slower |
| Tulip | 362.5 μs | 0 B | 1.14x slower |
| Skender | 75,147 μs | 50.8 MB | 236x slower |
| Ooples | 482,786 μs | 151 MB | 1,516x slower |
Exponential Moving Average (EMA)
QuanTAlib outperforms C library performance at 356 microseconds — significantly faster than Tulip's 709μs and TA-Lib's 707μs. Using Fused Multiply-Add (FMA) instructions for the hot path of EMA beats heavily optimized C code.
| Library | Mean Time | Allocations | Relative Speed |
|---|---|---|---|
| QuanTAlib (Span) | 355.9 μs | 0 B | 1.00x (baseline) |
| TA-Lib | 707.3 μs | 36 B | 1.99x slower |
| Tulip | 709.1 μs | 0 B | 1.99x slower |
| Skender | 31,085 μs | 50.8 MB | 87x slower |
| Ooples | 18,454 μs | 79.3 MB | 52x slower |
Weighted Moving Average (WMA)
QuanTAlib's WMA beats both C libraries — 313 microseconds versus Tulip's 377μs and TA-Lib's 364μs. This isn't a measurement error. Pure C# with proper SIMD vectorization outperforms C code that predates AVX-512 optimizations.
| Library | Mean Time | Allocations | Relative Speed |
|---|---|---|---|
| QuanTAlib (Span) | 312.7 μs | 0 B | 1.00x (baseline) |
| TA-Lib | 364.2 μs | 34 B | 1.16x slower |
| Tulip | 376.6 μs | 0 B | 1.20x slower |
| Skender | 103,489 μs | 50.8 MB | 331x slower |
| Ooples | 73,595 μs | 70.9 MB | 235x slower |
Hull Moving Average (HMA)
HMA requires multiple moving average calculations — traditionally expensive. QuanTAlib processes 500,000 bars in 963 microseconds. Tulip takes 2,272 microseconds. Skender requires 270,665 microseconds. (TALib doesn't include HMA calculation) That's a 2.36x improvement over optimized C and a 281x improvement over standard .NET implementations.
| Library | Mean Time | Allocations | Relative Speed |
|---|---|---|---|
| QuanTAlib (Span) | 963.3 μs | 0 B | 1.00x (baseline) |
| TA-Lib | -- | -- | -- |
| Tulip | 2,272.2 μs | 153 B | 2.36x slower |
| Skender | 270,665 μs | 235.9 MB | 281x slower |
| Ooples | 120,369 μs | 108.7 MB | 125x slower |
Multi-mode Comparison
The benchmarks above show Span mode. Here's how all four modes compare using EMA as representative:
| QuanTAlib Mode | Mean Time | Allocations | Use Case |
|---|---|---|---|
| Span | 355.9 μs | 0 B | Maximum speed, batch processing |
| Streaming | 464.2 μs | 42 B | Real-time updates, minimal overhead |
| Batch (TSeries) | 943.3 μs | 8.0 MB | Time-aligned series with metadata |
| Eventing | 3,055.5 μs | 16.8 MB | Reactive architectures with event infrastructure |
Even QuanTAlib's slowest mode (Eventing with complete event infrastructure and 16MB of allocations) processes 500,000 EMA values in 3.1 milliseconds — faster than Ooples' 18 milliseconds and Skender's 31 milliseconds for the same calculation.
Methodology
BenchmarkDotNet is used for all performance testing. This ensures:
- Warmup iterations to stabilize JIT compilation
- Statistical analysis of results (mean, standard deviation)
- Memory allocation tracking
- Environment isolation
How to Run Benchmarks Yourself
You can run the benchmarks on your own hardware to verify these results.
-
Clone the repository:
git clone https://github.com/mihakralj/QuanTAlib.git cd QuanTAlib -
Navigate to the performance project:
cd perf -
Run the benchmarks:
dotnet run -c ReleaseNote: Benchmarks must be run in Release configuration to enable optimizations.