Introduction

This article is a natural evolution of the article How to Sort Two-Dimensional Array in C# by Selected Column Index in Selected Column Sort Order where it is explained how to create custom two-dimensional array sort extensions.

Once we learn how to craft custom array sort extensions, a natural engineering question arises: how do they compare in execution speed and memory efficiency against the standard LINQ sorting methods widely used in modern software development?

This article explores how different memory layouts (T[,] vs T[][]) and sorting algorithms affect performance at scale. When dealing with large datasets containing hundreds of thousands or millions of rows with complex multi-level sorting criteria (alternating ASC/DESC across multiple columns with high tie frequencies), standard LINQ queries introduce significant Garbage Collector (GC) pressure and overhead. This article benchmarks traditional QuickSort implementations against optimized IntroSort (FastSort) and standard LINQ baselines using BenchmarkDotNet.

Benchmarking .NET Code: Methodology and Setup Reference Articles

When conducting performance measurements in .NET, naive approaches using Stopwatch are often flawed due to JIT compilation warm-ups, garbage collection interference, and CPU dead-code elimination. To ensure rigorous and scientifically reliable metrics, this article adopts BenchmarkDotNet, the industry standard for .NET benchmarking.

For readers interested in diving deeper into advanced benchmarking techniques, micro-optimization guidelines, and avoiding common pitfalls, the following official resources and guides are highly recommended:

The Benchmark Suite Overview

The complete source code and project files are available as a Public Solution on GitHub.

Our custom benchmarking suite is designed to pit our newly developed array sort extensions directly against the standard LINQ query pipeline. Specifically, the test scenario evaluates:

  1. Benchmark_2D_QuickSort: Evaluates the in-place QuickSort extension operating on traditional rectangular two-dimensional arrays (T[,]).

  2. Benchmark_Jagged_QuickSort: Measures the efficiency of pointer/reference swapping on jagged arrays (T[][]) using a custom QuickSort implementation.

  3. Benchmark_FastSort: Tests high-performance IntroSort combined with a zero-allocation struct comparer (JaggedRowComparer).

  4. Benchmark_LINQ (Baseline): Acts as the performance baseline ([Benchmark(Baseline = true)]), executing a multi-column .OrderBy().ThenBy() query pipeline over a list of lists (List<List<string>>).

To simulate heavy real-world workloads, all tests run across varying dataset scales—ranging from small sets of 100 rows up to massive scale tests with 1,000,000 rows—featuring 10 columns, with each individual cell containing a heavy payload of 1024 bytes, and high tie densities to stress multi-level sorting logic.

Final Results & Performance Analysis

Note on Benchmark Artifacts: Complete raw benchmark results, execution logs, HTML/CSV reports, and Markdown files generated by BenchmarkDotNet are fully preserved and available directly inside the BenchmarkDotNet.Artifacts/results folder of the public GitHub repository for transparent verification.

The following benchmark reports were generated using BenchmarkDotNet on .NET 10.0, utilizing a 13th Gen Intel Core i7-1360P 2.20GHz processor running Windows 11 with 64GB of RAM. The tests evaluate all sorting implementations across varying dataset scales—ranging from 100 to 1,000,000 rows—featuring 10 columns (where each individual cell contains a heavy payload of 1024 bytes) and high tie densities.

1. Dataset Scale: 100 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

40.40 µs

1.139 µs

3.287 µs

39.95 µs

1.01

0.11

1

-

12168 B

1.000

2D_QuickSort

115.89 µs

4.258 µs

12.420 µs

113.15 µs

2.89

0.39

4

-

7176 B

0.590

Jagged_QuickSort

54.67 µs

1.742 µs

5.109 µs

54.10 µs

1.36

0.17

2

-

-

0.000

FastSort

60.41 µs

1.655 µs

4.801 µs

60.60 µs

1.51

0.17

3

-

88 B

0.007

2. Dataset Scale: 1,000 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

417.59 µs

8.251 µs

9.502 µs

415.50 µs

1.00

0.03

1

-

102168 B

1.000

2D_QuickSort

1,006.69 µs

147.877 µs

431.364 µs

731.50 µs

2.41

1.03

2

-

68744 B

0.673

Jagged_QuickSort

524.06 µs

69.448 µs

203.678 µs

410.10 µs

1.26

0.49

1

-

-

0.000

FastSort

470.35 µs

56.852 µs

167.629 µs

352.45 µs

1.13

0.40

1

-

88 B

0.001

3. Dataset Scale: 10,000 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

4,328.80 µs

68.681 µs

60.884 µs

4,328.80 µs

1.00

0.02

1

-

1002168 B

1.000

2D_QuickSort

10,149.31 µs

199.454 µs

237.436 µs

10,133.30 µs

2.35

0.06

3

-

692432 B

0.691

Jagged_QuickSort

4,614.19 µs

91.555 µs

89.919 µs

4,607.70 µs

1.07

0.02

2

-

-

0.000

FastSort

4,695.23 µs

91.197 µs

89.567 µs

4,660.90 µs

1.08

0.02

2

-

88 B

0.000

4. Dataset Scale: 100,000 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

74,116.51 µs

1,448.398 µs

2,297.313 µs

74,773.40 µs

1.00

0.04

1

-

10002168 B

1.000

2D_QuickSort

187,779.30 µs

3,727.977 µs

9,488.892 µs

186,396.60 µs

2.54

0.15

2

-

6924216 B

0.692

Jagged_QuickSort

75,669.64 µs

1,511.808 µs

2,567.166 µs

75,341.85 µs

1.02

0.05

1

-

-

0.000

FastSort

74,298.62 µs

1,460.770 µs

1,999.519 µs

73,626.60 µs

1.00

0.04

1

-

88 B

0.000

5. Dataset Scale: 500,000 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

539,160.36 µs

6,715.002 µs

5,607.331 µs

538,875.50 µs

1.00

0.01

1

-

50002168 B

1.000

2D_QuickSort

1,646,896.16 µs

45,810.290 µs

133,630.774 µs

1,648,921.35 µs

3.05

0.25

3

3000.0000

34650616 B

0.693

Jagged_QuickSort

608,651.00 µs

11,884.140 µs

34,478.047 µs

612,560.80 µs

1.13

0.06

2

-

-

0.000

FastSort

595,113.19 µs

11,602.433 µs

16,264.992 µs

589,123.90 µs

1.10

0.03

2

-

88 B

0.000

6. Dataset Scale: 1,000,000 Rows

Method

Mean

Error

StdDev

Median

Ratio

RatioSD

Rank

Gen0

Allocated

Alloc Ratio

LINQ

1,255,436.79 µs

24,828.300 µs

23,224.408 µs

1,253,861.80 µs

1.00

0.03

1

-

100002168 B

1.000

2D_QuickSort

4,225,739.46 µs

100,507.544 µs

285,123.194 µs

4,181,847.60 µs

3.37

0.23

2

7000.0000

69336280 B

0.693

Jagged_QuickSort

1,334,138.06 µs

27,370.187 µs

80,701.620 µs

1,332,269.65 µs

1.06

0.07

1

-

-

0.000

FastSort

1,412,996.83 µs

17,510.803 µs

14,622.313 µs

1,416,142.70 µs

1.13

0.02

1

-

88 B

0.000

Key Takeaways from the Data

  • The Power of Zero Allocations (Jagged Arrays): Benchmark_Jagged_QuickSort consistently achieves zero heap allocations (- or 0 B) across all scales. By swapping row references instead of shifting heavy cell elements, it completely avoids heap allocations and minimizes Garbage Collection pressure.

  • FastSort and LINQ at Scale: For larger datasets (from 100k to 1M rows), FastSort and Jagged_QuickSort match standard LINQ performance closely (with a Ratio around 1.00 to 1.13), but with drastically reduced memory overhead. For example, at 1,000,000 rows, LINQ allocates over 95 MB (100002168 B), whereas FastSort uses a negligible 88 bytes.

  • The 2D Array Trade-off: Traditional rectangular 2D arrays (T[,]) exhibit higher execution times and greater memory allocations because partitioning requires physically moving individual elements across columns.

  • Fun Fact at Scale: To put these benchmark numbers into perspective, handling 1,000,000 rows with heavy 1,024-byte payloads per cell means your custom in-memory sorting engine can effortlessly organize the entire population of a country like Serbia, or scale up to over 10 million individuals—with zero GC pressure in just over a second! By implementing custom, in-place sorting extensions and zero-allocation struct comparers, developers can maintain lightning-fast execution times while keeping memory usage under strict control in enterprise-grade .NET environments.

Benchmark Implementation

The complete source code and project files are available as a Public Solution on GitHub.

The benchmarking solution is a simple console application structured around three core components managed via BenchmarkDotNet:

1. Program Entry Point (Program.cs): Configures and executes the benchmark runner with markdown export enabled.

2. Benchmark Suite (Benchmark.cs): Leverages [MemoryDiagnoser], [RankColumn], and [Params] attributes to evaluate sorting performance across dataset scales from 100 to 1,000,000 rows, isolating execution using [IterationSetup].

3. Synthetic Data Generator (RandomListGenerator.cs): Generates multi-column tabular datasets featuring high tie frequencies and heavy payloads to rigorously stress multi-level sorting logic.

The code inside the classes is thoroughly commented, so further explanation is unnecessary.

Program.cs

C#
using SortAndBenchmark;

class Program
{
    static void Main()
    {
        // Display startup console messages
        Console.WriteLine("======================================================================");
        Console.WriteLine(" STARTING BENCHMARK EXECUTION: LINQ vs Custom Sorting Algorithms");
        Console.WriteLine("======================================================================");
        Console.WriteLine("Initializing BenchmarkDotNet environment. Please wait...\n");

        // Configure BenchmarkDotNet with a custom artifacts path and markdown exporter
        var config = DefaultConfig.Instance
            .WithArtifactsPath("./BenchmarkDotNet.Artifacts")
            .AddExporter(MarkdownExporter.Default);

        // Run the benchmark suite with the specified configuration
        BenchmarkRunner.Run<Benchmark>(config);

        // Exit handling: keep console open until user presses a key
        Console.WriteLine("\nBenchmark execution finished successfully. Press any key to exit.");
        Console.ReadKey();

        Console.WriteLine("\nConfirm exiting console. Press any key to exit.");
        Console.ReadKey();
    }
}

Benchmark.cs

C#
using BenchmarkDotNet.Attributes;
using System.Collections.Generic;

namespace SortAndBenchmark
{
    /// <summary>
    /// Comprehensive benchmarking suite designed to measure pure sorting performance 
    /// (execution time and memory allocations) across multiple dataset scales (from 100 to 1,000,000 rows), 
    /// comparing custom in-place sorting algorithms against the standard LINQ query pipeline baseline.
    /// </summary>
    [MemoryDiagnoser]
    [RankColumn]
    public class Benchmark
    {
        /// <summary>
        /// Parameterized dataset row counts tested automatically by BenchmarkDotNet.
        /// Covers small, medium, and large scales to observe algorithmic complexity impact ($O(N \log N)$).
        /// </summary>
        [Params(100, 1000, 10000, 100000, 500000, 1000000)]
        public int Rows { get; set; }

        private const int _columns = 10;
        private SortDirective[] _sortDirectives = null!;
        private List<List<string>> _sourceList = null!;

        // Pre-allocated collection copies for each iteration to keep copying overhead completely out of benchmark methods.
        // Separate structures are maintained to test data layout impact (rectangular 2D array vs jagged array vs LINQ lists).
        private string[,] _array2D = null!;
        private string[][] _jaggedArray = null!;
        private string[][] _fastSortArray = null!;
        private List<List<string>> _linqList = null!;

        /// <summary>
        /// Initializes global benchmarking configurations and multi-column sorting directives.
        /// Executed once before the benchmark runs begin.
        /// </summary>
        [GlobalSetup]
        public void GlobalSetup()
        {
            // Define strict multi-column sorting rules matching all 10 columns with alternating directions (ASC/DESC)
            _sortDirectives =
            [
                new SortDirective(0, SortOrder.Ascending),  // Col 0: ASC
                new SortDirective(1, SortOrder.Descending), // Col 1: DESC
                new SortDirective(2, SortOrder.Ascending),  // Col 2: ASC
                new SortDirective(3, SortOrder.Descending), // Col 3: DESC
                new SortDirective(4, SortOrder.Ascending),  // Col 4: ASC
                new SortDirective(5, SortOrder.Ascending),  // Col 5: ASC
                new SortDirective(6, SortOrder.Descending), // Col 6: DESC
                new SortDirective(7, SortOrder.Ascending),  // Col 7: ASC
                new SortDirective(8, SortOrder.Descending), // Col 8: DESC
                new SortDirective(9, SortOrder.Ascending)   // Col 9: ASC
            ];
        }

        /// <summary>
        /// Generates fresh random data and populates dedicated test arrays prior to each iteration.
        /// The execution time and allocation overhead of this method are strictly excluded from benchmark measurements.
        /// </summary>
        [IterationSetup]
        public void IterationSetup()
        {
            _sourceList = RandomListGenerator.GenerateRandomList(Rows, _columns);

            // Populate test arrays outside the benchmark scope to ensure isolated sorting measurement
            _array2D = CopyListToArray(_sourceList);
            _jaggedArray = CopyListToJaggedArray(_sourceList);
            _fastSortArray = CopyListToJaggedArray(_sourceList);

            // Deep copy for LINQ to preserve source state and account for LINQ materialization overhead
            _linqList = new List<List<string>>(_sourceList.Count);
            for (int i = 0; i < _sourceList.Count; i++)
            {
                _linqList.Add(item: new List<string>(_sourceList[i]));
            }
        }

        /// <summary>
        /// Benchmarks traditional two-dimensional array (T[,]) QuickSort implementation.
        /// Measures pure in-place sorting logic and cache locality characteristics of rectangular arrays.
        /// </summary>
        [Benchmark]
        public void Benchmark_2D_QuickSort()
        {
            _array2D.QuickSortArray(_sortDirectives);
        }

        /// <summary>
        /// Benchmarks jagged array (T[][]) QuickSort implementation with reference pointer swapping.
        /// Measures sorting performance where row references are swapped rather than deep data elements.
        /// </summary>
        [Benchmark]
        public void Benchmark_Jagged_QuickSort()
        {
            _jaggedArray.QuickSortArray(_sortDirectives);
        }

        /// <summary>
        /// Benchmarks high-performance FastSort using .NET IntroSort combined with a 
        /// zero-allocation struct comparer. Measures optimized multi-column sorting logic with minimal GC pressure.
        /// </summary>
        [Benchmark]
        public void Benchmark_FastSort()
        {
            _fastSortArray.FastSortArray(_sortDirectives);
        }

        /// <summary>
        /// Benchmarks standard LINQ query pipeline with multi-column OrderBy/ThenBy sorting. 
        /// Designated as the performance baseline (Baseline = true) for relative comparison 
        /// and highlights LINQ's additional memory allocations due to object wrapping, delegates, and materialization.
        /// </summary>
        [Benchmark(Baseline = true)]
        public List<List<string>> Benchmark_LINQ()
        {
            return _linqList
                .OrderBy(row => row[0])
                .ThenByDescending(row => row[1])
                .ThenBy(row => row[2])
                .ThenByDescending(row => row[3])
                .ThenBy(row => row[4])
                .ThenBy(row => row[5])
                .ThenByDescending(row => row[6])
                .ThenBy(row => row[7])
                .ThenByDescending(row => row[8])
                .ThenBy(row => row[9])
                .ToList();
        }

        #region Helper Methods for Data Transformation

        // Converts standard list representation into a traditional 2D array (T[,])
        private static string[,] CopyListToArray(List<List<string>> list)
        {
            int rows = list.Count;
            int cols = list[0].Count;
            string[,] array = new string[rows, cols];

            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < cols; j++)
                {
                    array[i, j] = list[i][j];
                }
            }
            return array;
        }

        // Converts standard list representation into a jagged array (T[][])
        private static string[][] CopyListToJaggedArray(List<List<string>> list)
        {
            int rows = list.Count;
            string[][] array = new string[rows][];

            for (int i = 0; i < rows; i++)
            {
                array[i] = [.. list[i]];
            }
            return array;
        }

        #endregion
    }
}

RandomListGenerator.cs

C#
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Running;
using SortAndBenchmark;

class Program
{
    static void Main()
    {
        // Display startup console messages
        Console.WriteLine("======================================================================");
        Console.WriteLine(" STARTING BENCHMARK EXECUTION: LINQ vs Custom Sorting Algorithms");
        Console.WriteLine("======================================================================");
        Console.WriteLine("Initializing BenchmarkDotNet environment. Please wait...\n");

        // Configure BenchmarkDotNet to export results into multiple file formats
        var config = DefaultConfig.Instance
            .AddExporter(MarkdownExporter.Default); // Export results to Markdown file

        // Run the benchmark suite with the specified configuration
        BenchmarkRunner.Run<Benchmark>(config);

        // Exit handling: keep console open until user presses a key
        Console.WriteLine("\nBenchmark execution finished successfully. Press any key to exit.");
        Console.ReadKey();

        Console.WriteLine("\nConfirm exiting console. Press any key to exit.");
        Console.ReadKey();
    }
}

Sort Extensions Implementation

The following code provides high-performance extension methods for sorting multi-dimensional (2D) and jagged arrays using advanced sorting algorithms (QuickSort, IntroSort) and custom multi-column directives.

SortExtensions.cs

C#
using System.Runtime.CompilerServices;

namespace SortAndBenchmark
{
    /// <summary>
    /// Specifies the direction of a sort operation.
    /// </summary>
    public enum SortOrder
    {
        Ascending,
        Descending
    }

    /// <summary>
    /// Represents a single sorting rule defining the target column index and sort order.
    /// </summary>
    public readonly struct SortDirective(int column, SortOrder order)
    {
        public int Column { get; } = column;
        public SortOrder Order { get; } = order;
    }

    /// <summary>
    /// Provides high-performance extension methods for sorting multi-dimensional (2D) 
    /// and jagged arrays using advanced sorting algorithms and custom multi-column directives.
    /// </summary>
    public static class SortExtensions
    {
        #region Fast Sort (Jagged Arrays)

        /// <summary>
        /// Sorts a jagged array using the .NET IntroSort algorithm paired with a zero-allocation 
        /// struct comparer. This method provides maximum performance.
        /// </summary>
        /// <typeparam name="T">The type of elements in the array, must implement IComparable{T}.</typeparam>
        /// <param name="array">The jagged array of rows to be sorted.</param>
        /// <param name="directives">The sorting directives specifying column indices and sort orders.</param>
        public static void FastSortArray<T>(this T[][] array, params SortDirective[] directives)
            where T : IComparable<T>
        {
            // Guard clause: Exit early if the array is null, has 1 or fewer rows, or no directives
            if (array == null || array.Length <= 1 || directives == null || directives.Length == 0)
                return;

            // Invoke optimized .NET Array.Sort (IntroSort) with a zero-allocation struct comparer
            Array.Sort(array, new JaggedRowComparer<T>(directives));
        }

        #endregion

        #region Quick Sort for Jagged Arrays (High Performance)

        /// <summary>
        /// Sorts a jagged array based on multi-column sort directives using the QuickSort algorithm.
        /// Swaps row references instead of copying elements individually, offering high performance.
        /// </summary>
        /// <typeparam name="T">The type of elements in the array.</typeparam>
        /// <param name="array">The jagged array to sort.</param>
        /// <param name="directives">The sorting rules specifying column index and order.</param>
        public static void QuickSortArray<T>(this T[][] array, params SortDirective[] directives)
            where T : IComparable<T>
        {
            // Exit if the array is empty, has a single row, or no directives
            if (array == null || array.Length <= 1 || directives == null || directives.Length == 0)
                return;

            // Start the recursive QuickSort partitioning process for jagged arrays
            QuickSortJagged(array, directives, 0, array.Length - 1);
        }

        // Core recursive QuickSort method for jagged arrays
        private static void QuickSortJagged<T>(T[][] array, SortDirective[] directives, int low, int high)
            where T : IComparable<T>
        {
            // Continue sorting only if the segment has more than one element
            if (low < high)
            {
                // Partition the array around a pivot and get its final sorted index
                int pi = PartitionJagged(array, directives, low, high);

                // Recursively sort elements before and after the partition index
                QuickSortJagged(array, directives, low, pi - 1);
                QuickSortJagged(array, directives, pi + 1, high);
            }
        }

        // Partitions the jagged array segment using the last row as a pivot
        private static int PartitionJagged<T>(T[][] array, SortDirective[] directives, int low, int high)
            where T : IComparable<T>
        {
            // Select the last row as the pivot reference
            T[] pivot = array[high];
            int i = low - 1; // Index of smaller element

            // Scan through the segment and rearrange rows based on multi-column rules
            for (int j = low; j < high; j++)
            {
                // Check if current row should precede the pivot
                if (IsBeforeJagged(array[j], pivot, directives))
                {
                    i++;
                    SwapJaggedRows(array, i, j); // Swap rows via reference pointers
                }
            }

            // Place the pivot into its correct sorted position
            SwapJaggedRows(array, i + 1, high);
            return i + 1; // Return the partition index
        }

        // Evaluates multi-column sorting precedence between a row and the pivot
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool IsBeforeJagged<T>(T[] row, T[] pivot, SortDirective[] directives)
            where T : IComparable<T>
        {
            // Iterate through each sorting directive sequentially (primary, secondary, etc.)
            for (int i = 0; i < directives.Length; i++)
            {
                ref readonly var directive = ref directives[i];
                int col = directive.Column;
                SortOrder order = directive.Order;

                // Compare individual cell values at the target column
                int comparison = row[col].CompareTo(pivot[col]);

                // If values are not equal, determine precedence based on sort order
                if (comparison != 0)
                {
                    return order == SortOrder.Ascending ? comparison < 0 : comparison > 0;
                }
            }
            // If all columns are identical, keep original relative order
            return true;
        }

        // Extremely fast row swapping via tuple assignment (no cell copying)
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void SwapJaggedRows<T>(T[][] array, int i, int j)
        {
            (array[j], array[i]) = (array[i], array[j]);
        }

        #endregion

        #region Quick Sort for Traditional 2D Arrays (T[,])

        /// <summary>
        /// Sorts a traditional two-dimensional array based on sort directives using QuickSort.
        /// </summary>
        /// <typeparam name="T">The type of elements in the array.</typeparam>
        /// <param name="array">The 2D array to sort.</param>
        /// <param name="directives">The sorting directives.</param>
        public static void QuickSortArray<T>(this T[,] array, params SortDirective[] directives)
            where T : IComparable<T>
        {
            // Guard clause for 2D array row count or directives
            if (array == null || array.GetLength(0) <= 1 || directives == null || directives.Length == 0)
                return;

            // Initiate recursive 2D QuickSort
            QuickSort2D(array, directives, 0, array.GetLength(0) - 1);
        }

        // Recursive helper for 2D array QuickSort
        private static void QuickSort2D<T>(T[,] array, SortDirective[] directives, int low, int high)
            where T : IComparable<T>
        {
            if (low < high)
            {
                int pi = Partition2D(array, directives, low, high);

                QuickSort2D(array, directives, low, pi - 1);
                QuickSort2D(array, directives, pi + 1, high);
            }
        }

        // Partitions a traditional 2D array by copying the pivot row into a temporary 1D buffer
        private static int Partition2D<T>(T[,] array, SortDirective[] directives, int low, int high)
            where T : IComparable<T>
        {
            int cols = array.GetLength(1);
            T[] pivot = new T[cols];

            // Extract the pivot row from the 2D array
            for (int k = 0; k < cols; k++)
            {
                pivot[k] = array[high, k];
            }

            int i = low - 1;

            for (int j = low; j < high; j++)
            {
                if (IsBefore2D(array, directives, j, pivot))
                {
                    i++;
                    SwapRows2D(array, i, j); // Physically swaps cells column by column
                }
            }

            SwapRows2D(array, i + 1, high);
            return i + 1;
        }

        // Evaluates sorting precedence for 2D array elements
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool IsBefore2D<T>(T[,] array, SortDirective[] directives, int row, T[] pivot)
            where T : IComparable<T>
        {
            for (int i = 0; i < directives.Length; i++)
            {
                ref readonly var directive = ref directives[i];
                int column = directive.Column;
                SortOrder order = directive.Order;

                int comparison = array[row, column].CompareTo(pivot[column]);

                if (comparison != 0)
                {
                    if ((order == SortOrder.Ascending && comparison > 0) || (order == SortOrder.Descending && comparison < 0))
                    {
                        return false;
                    }
                    else if ((order == SortOrder.Descending && comparison > 0) || (order == SortOrder.Ascending && comparison < 0))
                    {
                        return true;
                    }
                }
            }
            return true;
        }

        // Physically swaps all individual elements across columns for two rows in a 2D array
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void SwapRows2D<T>(T[,] array, int i, int j)
        {
            int cols = array.GetLength(1);
            for (int col = 0; col < cols; col++)
            {
                (array[j, col], array[i, col]) = (array[i, col], array[j, col]);
            }
        }

        #endregion

        #region Selection Sort for Traditional 2D Arrays (T[,])

        /// <summary>
        /// Sorts a traditional two-dimensional array using the Selection Sort algorithm 
        /// based on multi-column sorting directives.
        /// </summary>
        /// <typeparam name="T">The type of elements in the array.</typeparam>
        /// <param name="array">The 2D array to sort.</param>
        /// <param name="directives">The sorting directives.</param>
        public static void SelectionSortArray<T>(this T[,] array, params SortDirective[] directives)
            where T : IComparable<T>
        {
            // Guard clause for null or single-row 2D arrays
            if (array == null || array.GetLength(0) <= 1 || directives == null || directives.Length == 0)
                return;

            int arrayRows = array.GetLength(0);
            int arrayColumns = array.GetLength(1);
            int sortDirectiveCount = directives.Length;

            // Selection sort outer loop
            for (int i = 0; i < arrayRows - 1; i++)
            {
                int minIndex = i;

                // Inner loop to find the extreme element according to multi-column rules
                for (int j = i + 1; j < arrayRows; j++)
                {
                    bool shouldSwap = false;

                    for (int c = 0; c < sortDirectiveCount; c++)
                    {
                        ref readonly var directive = ref directives[c];
                        int columnToSort = directive.Column;
                        SortOrder sortOrder = directive.Order;

                        T valueI = array[minIndex, columnToSort];
                        T valueJ = array[j, columnToSort];

                        int comparisonResult = valueI.CompareTo(valueJ);

                        int compRule = sortOrder == SortOrder.Descending ? -comparisonResult : comparisonResult;
                        if (compRule < 0)
                        {
                            shouldSwap = true;
                            break;
                        }
                        else if (compRule > 0)
                        {
                            break;
                        }
                    }

                    if (shouldSwap)
                    {
                        minIndex = j;
                    }
                }

                // Swap rows if a new minimum/maximum row was found
                if (minIndex != i)
                {
                    for (int d = 0; d < arrayColumns; d++)
                    {
                        (array[i, d], array[minIndex, d]) = (array[minIndex, d], array[i, d]);
                    }
                }
            }
        }

        #endregion

        #region Shared Supporting Structures

        /// <summary>
        /// A high-performance, zero-allocation struct-based comparer for jagged array rows.
        /// Enables JIT compiler devirtualization and inlining for maximum execution speed.
        /// </summary>
        private readonly struct JaggedRowComparer<T>(SortDirective[] directives) : IComparer<T[]>
            where T : IComparable<T>
        {
            // Custom comparison logic executed millions of times during sorting
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public int Compare(T[]? rowA, T[]? rowB)
            {
                // Handle null reference edge cases
                if (rowA == rowB)
                    return 0;
                if (rowA == null)
                    return -1;
                if (rowB == null)
                    return 1;

                int length = directives.Length;

                // Iterate through multi-column directives sequentially
                for (int i = 0; i < length; i++)
                {
                    ref readonly var directive = ref directives[i];
                    int col = directive.Column;
                    SortOrder order = directive.Order;

                    int comparison = rowA[col].CompareTo(rowB[col]);

                    // Return comparison outcome adjusted by sort direction
                    if (comparison != 0)
                    {
                        return order == SortOrder.Ascending ? comparison : -comparison;
                    }
                }
                return 0;
            }
        }

        #endregion
    }
}

Conclusion

When building high-performance .NET applications that process large-scale tabular datasets, choosing the right data structure and sorting strategy is critical. As demonstrated by our BenchmarkDotNet analysis:

Zero GC Allocations with Jagged Arrays: By utilizing jagged arrays (T[][]) and swapping row references rather than shifting heavy data elements, custom sorters achieve 0 Bytes allocated across all dataset scales, completely removing Garbage Collection pressure.

Beating LINQ's Memory Footprint: While standard LINQ queries (OrderBy/ThenBy) offer great readability for everyday tasks, they allocate massive amounts of memory on the managed heap at scale (over 100 MB for 1 million rows). Optimized custom sorters like FastSort match LINQ's execution speed while consuming a negligible fraction of memory (only 88 bytes).

Data Layout Matters: Traditional rectangular 2D arrays (T[,]) suffer from increased overhead due to physical element swapping across columns, making jagged arrays the preferred architectural choice for performance-critical matrix operations.

By implementing custom, in-place sorting extensions and zero-allocation struct comparers, developers can maintain lightning-fast execution times while keeping memory usage under strict control in enterprise-grade .NET environments.