.NET  

.NET 11 Generic Complex Numbers: Benchmarking Complex for Scientific Workloads

Introduction

Complex numbers are common in scientific computing, signal processing, engineering, simulations, graphics, and numerical algorithms.

.NET has supported complex numbers through System.Numerics.Complex for a long time. That type uses double values internally, which works well for many applications but is not ideal when a calculation needs a different numeric representation.

Generic math changes that picture.

The generic Complex<T> type in .NET 11 allows complex numbers to work with a generic real-number type rather than being tied to double. This makes it possible to use complex arithmetic with numeric types such as float or other supported numeric representations.

For developers working on numerical software, the interesting question is not simply whether generic complex numbers are more flexible. It is how different numeric types affect execution time, memory usage, and numerical behavior.

This article explains the concept behind Complex<T> and shows how to design a benchmark for scientific workloads without assuming a performance improvement before measuring it.

What Is a Complex Number?

A complex number contains two components:

a + bi

where:

  • a is the real component.

  • b is the imaginary component.

  • i represents the imaginary unit.

For example:

3 + 4i

has:

Real      = 3
Imaginary = 4

Complex numbers are useful when mathematical operations need to represent both magnitude and phase.

A common example is signal processing, where a signal can be represented using real and imaginary components.

Traditional Complex in .NET

.NET provides System.Numerics.Complex.

A basic example looks like this:

using System.Numerics;

Complex value = new(3, 4);

Complex result = value * 2;

Console.WriteLine(result);

The traditional Complex type uses double-precision components.

That is convenient when double is the appropriate numeric representation, but scientific applications do not always have the same requirements.

Some workloads prefer float because the lower precision is sufficient and the smaller representation can be useful for large collections of numerical values.

This is where a generic complex representation becomes interesting.

What Is Complex?

The generic approach allows the underlying real-number type to be specified.

Conceptually:

Complex<float>

represents a complex number whose real and imaginary components use float.

Similarly:

Complex<double>

can represent complex values using double.

The generic form can therefore be thought of as:

Complex<T>
    |
    +-- Real: T
    |
    +-- Imaginary: T

This separates the concept of a complex number from one specific numeric precision.

The exact supported numeric types and APIs should be verified against the .NET 11 SDK being evaluated because the generic numeric APIs are part of the evolving .NET implementation.

Why Generic Numeric Types Matter

Consider an application processing millions of complex values.

With a double-based representation, each component requires 8 bytes.

A complex value containing two double components therefore requires approximately:

8 bytes + 8 bytes = 16 bytes

A float uses 4 bytes per component:

4 bytes + 4 bytes = 8 bytes

For a large collection, that difference can become significant.

For example, a conceptual array containing 10 million values would require roughly:

Double-based complex values
10,000,000 × 16 bytes ≈ 160 MB

Float-based complex values
10,000,000 × 8 bytes ≈ 80 MB

These are simple storage calculations, not benchmark results. Actual application memory usage can include array overhead, other objects, buffers, alignment, and temporary allocations.

The trade-off is that float provides less precision than double.

Precision vs Memory

Choosing a numeric type is therefore a scientific and engineering decision.

Numeric TypeTypical PrecisionStorage per ValueCommon Consideration
floatLower4 bytesMemory-sensitive workloads
doubleHigher8 bytesGeneral scientific calculations
Other numeric typesDependsDependsSpecialized requirements

For a complex number containing two components, the storage is approximately twice the size of the underlying scalar type.

The smallest representation is not automatically the best one.

If a scientific algorithm accumulates rounding errors over many iterations, using a lower-precision type may produce unacceptable results.

A Simple Generic Complex Calculation

A scientific workload might perform repeated complex multiplication.

For example:

static Complex<float> Multiply(
    Complex<float> left,
    Complex<float> right)
{
    return left * right;
}

A workload can repeatedly apply the operation:

static Complex<float> Calculate(
    Complex<float> value,
    int iterations)
{
    var result = value;

    for (int i = 0; i < iterations; i++)
    {
        result *= value;
    }

    return result;
}

The same algorithm can be evaluated with another supported numeric type.

The important part of a benchmark is keeping the algorithm identical.

Building a Benchmark

BenchmarkDotNet is a good choice for measuring CPU and allocation behavior.

A benchmark can start with:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class ComplexBenchmark
{
    private readonly Complex<float> floatValue =
        new(1.25f, 2.5f);

    [Benchmark]
    public Complex<float> FloatCalculation()
    {
        var result = floatValue;

        for (int i = 0; i < 1_000; i++)
        {
            result *= floatValue;
        }

        return result;
    }
}

The benchmark should be compiled and executed using the Release configuration:

dotnet run -c Release

The exact benchmark code may need adjustment for the API surface available in the specific .NET 11 SDK preview being evaluated.

Comparing Float and Double

A more useful benchmark compares equivalent workloads.

Conceptually:

[Benchmark]
public Complex<float> FloatCalculation()
{
    var result = floatValue;

    for (int i = 0; i < Iterations; i++)
    {
        result *= floatValue;
    }

    return result;
}

[Benchmark]
public Complex<double> DoubleCalculation()
{
    var result = doubleValue;

    for (int i = 0; i < Iterations; i++)
    {
        result *= doubleValue;
    }

    return result;
}

The benchmark should keep the following identical:

  • Number of iterations

  • Mathematical operation

  • Input values where appropriate

  • Runtime version

  • Compiler configuration

  • Hardware

  • Benchmark process

Do not report a performance winner simply because one run happened to complete faster.

Measuring Memory

If the application stores large arrays of complex numbers, memory usage becomes an important metric.

For example:

private Complex<float>[] floatValues =
    new Complex<float>[1_000_000];

and:

private Complex<double>[] doubleValues =
    new Complex<double>[1_000_000];

The arrays have different storage requirements because the underlying components have different sizes.

BenchmarkDotNet's memory diagnoser can help identify managed allocations:

[MemoryDiagnoser]
public class ComplexMemoryBenchmark
{
    [Benchmark]
    public Complex<float>[] CreateFloatArray()
    {
        return new Complex<float>[1_000_000];
    }

    [Benchmark]
    public Complex<double>[] CreateDoubleArray()
    {
        return new Complex<double>[1_000_000];
    }
}

The benchmark should be interpreted carefully.

A large preallocated array may be useful for measuring representation size, but it does not represent the complete memory profile of a scientific application.

Benchmarking a Realistic Workload

A simple multiplication loop is useful for understanding basic arithmetic, but real scientific applications usually do more.

Examples include:

  • Fast Fourier Transform calculations

  • Signal filtering

  • Frequency analysis

  • Matrix operations

  • Simulation

  • Numerical optimization

  • Scientific modeling

A benchmark should eventually resemble the application's actual algorithm.

For example, if the application performs an FFT over batches of signal data, benchmarking only scalar complex multiplication may not tell you whether Complex<float> is a good choice.

The memory layout, vectorization, cache behavior, and surrounding algorithm can all affect the result.

Numerical Accuracy Must Be Tested

Performance should never be evaluated independently from numerical correctness.

Consider two implementations:

Implementation A
float

and:

Implementation B
double

Even if A requires less memory or completes faster, it may accumulate more rounding error.

A simple test can compare the outputs:

double difference =
    Math.Abs(expected - actual);

For floating-point calculations, use an appropriate tolerance rather than expecting exact equality.

For example:

Assert.True(
    Math.Abs(expected - actual) < tolerance);

The correct tolerance depends on the algorithm and its numerical stability.

There is no universal tolerance that is appropriate for every scientific calculation.

Overflow and Underflow

Changing numeric precision can also change numerical behavior.

Repeated calculations can produce values that exceed the representable range or become too small to represent accurately.

For example:

var result = value;

for (int i = 0; i < iterations; i++)
{
    result *= value;
}

As iterations increases, the magnitude of the result can change dramatically.

A benchmark should therefore check whether the output remains meaningful rather than measuring only execution time.

Vectorization and Hardware Effects

Modern processors can perform numerical operations using SIMD instructions.

That means the performance of a numeric type can depend on:

  • CPU architecture

  • Runtime implementation

  • JIT optimizations

  • Data layout

  • Algorithm structure

  • Vectorization opportunities

A result observed on one processor should not automatically be treated as a universal characteristic of float or double.

This is another reason to benchmark on hardware representative of the application.

Common Mistakes

Choosing Float Only Because It Uses Less Memory

Lower memory usage can be valuable, but reduced precision may affect the algorithm's correctness.

Benchmarking Only Scalar Arithmetic

A real scientific workload may be dominated by memory access, matrix operations, FFT processing, or other algorithms.

Ignoring Numerical Error

A faster result is not useful if the numerical output is outside the acceptable error range.

Comparing Different Algorithms

The float and double implementations must perform equivalent work.

Publishing Hardware-Specific Results as Universal

CPU architecture and runtime optimizations influence numerical performance.

Troubleshooting Unexpected Results

If a generic complex benchmark performs differently than expected, investigate:

  1. Numeric type.

  2. CPU architecture.

  3. JIT optimization.

  4. Release vs Debug configuration.

  5. Number of iterations.

  6. Memory allocation.

  7. Data size.

  8. Cache behavior.

  9. Algorithm complexity.

  10. Numerical overflow or underflow.

If allocations are unexpectedly high, inspect whether the benchmark creates temporary objects or collections inside the measured method.

If performance changes significantly when the dataset grows, investigate whether the workload has moved from CPU-bound to memory-bound behavior.

Production Considerations

Before replacing an existing Complex implementation, evaluate the actual application requirements.

Ask:

  • How much precision is required?

  • How large are the datasets?

  • Is memory a limiting factor?

  • Is the application CPU-bound?

  • Does the algorithm accumulate numerical error?

  • Are third-party libraries compatible with the selected representation?

  • Does the workload benefit from a smaller data representation?

For a small scientific calculation, switching from double to float may provide little practical benefit.

For very large datasets, however, reducing the size of each numerical value can have a much larger impact on memory consumption and data movement.

Best Practices

Start With Numerical Requirements

Determine the required precision before optimizing storage or CPU performance.

Benchmark Real Algorithms

Test the workload users actually run.

Measure Memory and CPU Together

A smaller representation may reduce memory pressure while changing computational behavior.

Validate Numerical Accuracy

Compare outputs against a trusted reference implementation.

Use Release Builds

Debug builds do not provide reliable performance information.

Record the Environment

Document:

  • .NET SDK version

  • Operating system

  • CPU

  • Memory

  • Benchmark configuration

  • Dataset

  • Number of iterations

This makes results easier to reproduce.

Advantages

  • Allows complex-number algorithms to work with different numeric representations.

  • Provides more flexibility than a type tied exclusively to double.

  • Can reduce memory requirements when a smaller numeric type is appropriate.

  • Fits naturally with .NET's broader generic math direction.

  • Makes it possible to benchmark precision and performance as separate design choices.

Disadvantages

  • Lower-precision types can produce greater numerical error.

  • Performance depends on the algorithm and hardware.

  • Generic APIs can make existing code more complex during migration.

  • Third-party numerical libraries may expect a particular numeric representation.

  • Memory savings do not automatically translate into faster execution.

Conclusion

Generic complex numbers provide an important option for developers building numerical applications in .NET.

The biggest advantage of Complex<T> is flexibility. Developers can choose an appropriate underlying numeric representation instead of assuming that every complex calculation needs the same precision.

That flexibility also means developers need to make a more deliberate engineering decision.

A float-based implementation may reduce memory consumption for large datasets, while double may provide the numerical stability required by a particular algorithm. Neither choice should be considered universally better.

The right way to evaluate the difference is to benchmark an equivalent workload, measure memory and execution time, and validate numerical accuracy at the same time.

For scientific applications, performance is only one part of correctness. The best implementation is the one that provides acceptable numerical accuracy, resource usage, and execution characteristics for the actual workload.