Insert and Sort - which approach is actually the best?

Elijah Koulaxis

November 20, 2025

Recently I faced an interesting challenge:
I had to continuously insert a large number of objects and immediately retrieve them in sorted (descending) order, every single time an insertion happened

loop {
    insert -> sort -> return
}

My first thought was simple: just use a List<T>

But I started wondering… can we actually do better?

Trying PriorityQueue

In theory, this should win..

But .NET has its own ecosystem, its own optimizations, and its own quirks. You never really know until you benchmark

So I did exactly that.

Benchmark Results (List vs PriorityQueue)

| Method               | N       | Mean             | Error          | StdDev         | Gen0     | Gen1     | Gen2     | Allocated  |
|--------------------- |-------- |-----------------:|---------------:|---------------:|---------:|---------:|---------:|-----------:|
| ListSort             | 10      |         49.54 ns |       0.642 ns |       0.536 ns |   0.0006 |        - |        - |       96 B |
| PriorityQueueMaxHeap | 10      |         82.35 ns |       1.666 ns |       3.622 ns |   0.0021 |        - |        - |      344 B |

| ListSort             | 100     |        463.78 ns |       9.013 ns |      12.635 ns |   0.0024 |        - |        - |      456 B |
| PriorityQueueMaxHeap | 100     |        631.30 ns |      12.504 ns |      23.485 ns |   0.0134 |        - |        - |     2208 B |

| ListSort             | 1000    |      8,502.69 ns |     166.158 ns |     155.424 ns |   0.0153 |        - |        - |     4056 B |
| PriorityQueueMaxHeap | 1000    |      4,972.78 ns |      98.582 ns |     266.522 ns |   0.0992 |        - |        - |    16616 B |

| ListSort             | 10000   |    298,924.86 ns |   4,962.533 ns |   4,641.956 ns |        - |        - |        - |    40056 B |
| PriorityQueueMaxHeap | 10000   |    145,681.72 ns |   2,868.882 ns |   4,114.465 ns |   4.3945 |   3.9063 |   3.9063 |   262491 B |

| ListSort             | 100000  |  4,296,777.64 ns |  52,767.400 ns |  49,358.659 ns |   7.8125 |   7.8125 |   7.8125 |   400061 B |
| PriorityQueueMaxHeap | 100000  |  1,924,881.06 ns |  37,111.871 ns |  34,714.467 ns |  46.8750 |  46.8750 |  46.8750 |  2097674 B |

| ListSort             | 1000000 | 49,893,336.22 ns | 322,795.615 ns | 286,149.905 ns |        - |        - |        - |  4000056 B |
| PriorityQueueMaxHeap | 1000000 | 15,571,538.49 ns | 290,155.221 ns | 284,971.224 ns | 156.2500 | 156.2500 | 156.2500 | 16777787 B |

Observations

We can observe the following:

And this is because the dotnet team behind Microsoft has done an incredible job at optimizing lists as much as possible. Whereas for a PriorityQueue you have more fields per entry, you've got comparer logic, resizing, metadata and structures that allocate on the heap

So I thought to myself, I know that PriorityQueue is conceptually the right structure, so what if I just implement my own max-heap manually and essentially ditch all that unnecessary work, for my case at least, that PriorityQueue class does behind the scenes, and use a simple array with the classic heapify logic

If you're not familiar with how heapify works here's a trivial diagram:

heapify-logic

Our custom heap is just:

There is no priority tuple, no comparer, no metadata, nothing.

It's just a pure max-heap using the classic parent/child index formula:

parent = (i - 1) / 2
left   = 2*i + 1
right  = 2*i + 2

The FastMaxHeap Implementation

The code would look like this:

public class FastMaxHeap
{
    private int[] _data;
    private int _count;

    public FastMaxHeap(int capacity)
    {
        _data = new int[capacity];
        _count = 0;
    }

    public int Max => _data[0];

    public void Add(int value)
    {
        int i = _count++;
        _data[i] = value;

        while (i > 0)
        {
            int parent = (i - 1) >> 1;
            if (_data[parent] >= _data[i])
            {
                break;
            }

            (_data[parent], _data[i]) = (_data[i], _data[parent]);

            i = parent;
        }
    }
}

Benchmark Results (Including Custom Heap)

And the results were even more interesting:

| Method               | N       | Mean             | Error          | StdDev         | Gen0     | Gen1     | Gen2     | Allocated  |
|--------------------- |-------- |-----------------:|---------------:|---------------:|---------:|---------:|---------:|-----------:|
| ListSort             | 10      |         49.54 ns |       0.642 ns |       0.536 ns |   0.0006 |        - |        - |       96 B |
| PriorityQueueMaxHeap | 10      |         82.35 ns |       1.666 ns |       3.622 ns |   0.0021 |        - |        - |      344 B |
| FastMaxHeapInsert    | 10      |         35.92 ns |       0.729 ns |       1.352 ns |   0.0005 |        - |        - |       96 B |

| ListSort             | 100     |        463.78 ns |       9.013 ns |      12.635 ns |   0.0024 |        - |        - |      456 B |
| PriorityQueueMaxHeap | 100     |        631.30 ns |      12.504 ns |      23.485 ns |   0.0134 |        - |        - |     2208 B |
| FastMaxHeapInsert    | 100     |        317.84 ns |       6.196 ns |       6.887 ns |   0.0024 |        - |        - |      456 B |

| ListSort             | 1000    |      8,502.69 ns |     166.158 ns |     155.424 ns |   0.0153 |        - |        - |     4056 B |
| PriorityQueueMaxHeap | 1000    |      4,972.78 ns |      98.582 ns |     266.522 ns |   0.0992 |        - |        - |    16616 B |
| FastMaxHeapInsert    | 1000    |      3,102.24 ns |      58.571 ns |      54.787 ns |   0.0229 |        - |        - |     4056 B |

| ListSort             | 10000   |    298,924.86 ns |   4,962.533 ns |   4,641.956 ns |        - |        - |        - |    40056 B |
| PriorityQueueMaxHeap | 10000   |    145,681.72 ns |   2,868.882 ns |   4,114.465 ns |   4.3945 |   3.9063 |   3.9063 |   262491 B |
| FastMaxHeapInsert    | 10000   |     32,425.53 ns |     644.140 ns |   1,330.261 ns |   0.2441 |        - |        - |    40056 B |

| ListSort             | 100000  |  4,296,777.64 ns |  52,767.400 ns |  49,358.659 ns |   7.8125 |   7.8125 |   7.8125 |   400061 B |
| PriorityQueueMaxHeap | 100000  |  1,924,881.06 ns |  37,111.871 ns |  34,714.467 ns |  46.8750 |  46.8750 |  46.8750 |  2097674 B |
| FastMaxHeapInsert    | 100000  |  1,096,367.59 ns |  11,521.541 ns |  10,777.257 ns |   9.7656 |   9.7656 |   9.7656 |   400125 B |

| ListSort             | 1000000 | 49,893,336.22 ns | 322,795.615 ns | 286,149.905 ns |        - |        - |        - |  4000056 B |
| PriorityQueueMaxHeap | 1000000 | 15,571,538.49 ns | 290,155.221 ns | 284,971.224 ns | 156.2500 | 156.2500 | 156.2500 | 16777787 B |
| FastMaxHeapInsert    | 1000000 | 10,713,933.71 ns | 148,080.628 ns | 131,269.619 ns |  46.8750 |  46.8750 |  46.8750 |  4000086 B |

The key take-aways from this article are two:

So, if you want raw performance + minimal allocations you may as well do it yourself It's a perfect choice for real-time systems, games etc

Tags:
Back to Home