title: C#/.NET - Performance degrading with minimally-parallelized Quicksort
body: <p>I am currently working on a recursively parallel Quicksort extension function for the List class. The code below represents the most basic thread distribution criteria I've considered because it should be the simplest to conceptually explain. It branches to the depth of the base-2 logarithm of the number of detected processors, and proceeds sequentially from there. Thus, each CPU should get one thread with a (roughly) equal, large share of data to process, avoiding excessive overhead time. The basic sequential algorithm is provided for comparison.</p>
  
  <pre><code>public static class Quicksort
  {
      /// &lt;summary&gt;
      /// Helper class to hold information about when to parallelize
      /// &lt;/summary&gt;
      /// &lt;attribute name="maxThreads"&gt;Maximum number of supported threads&lt;/attribute&gt;
      /// &lt;attribute name="threadDepth"&gt;The depth to which new threads should
      ///                               automatically be made&lt;/attribute&gt;
      private class ThreadInfo
      {
          internal int maxThreads;
          internal int threadDepth;
  
          public ThreadInfo(int length)
          {
              maxThreads = Environment.ProcessorCount;
              threadDepth = (int)Math.Log(maxThreads, 2);
          }
  
      }
  
      /// &lt;summary&gt;
      /// Helper function to perform the partitioning step of quicksort
      /// &lt;/summary&gt;
      /// &lt;param name="list"&gt;The list to partition&lt;/param&gt;
      /// &lt;param name="start"&gt;The starting index&lt;/param&gt;
      /// &lt;param name="end"&gt;The ending index/param&gt;
      /// &lt;returns&gt;The final index of the pivot&lt;/returns&gt;
      public static int Partition&lt;T&gt;(this List&lt;T&gt; list, int start, int end) where T: IComparable
      {
          int middle = (int)(start + end) / 2;
  
          // Swap pivot and first item.
          var temp = list[start];
          list[start] = list[middle];
          list[middle] = temp;
          var pivot = list[start];
  
          var swapPtr = start;
  
          for (var cursor = start + 1; cursor &lt;= end; cursor++)
          {
              if (list[cursor].CompareTo(pivot) &lt; 0)
              {
                  // Swap cursor element and designated swap element
                  temp = list[cursor];
                  list[cursor] = list[++swapPtr];
                  list[swapPtr] = temp;
              }
          }
  
          // Swap pivot with final lower item
          temp = list[start];
          list[start] = list[swapPtr];
          list[swapPtr] = temp;
  
          return swapPtr;
      }
  
      /// &lt;summary&gt;
      /// Method to begin parallel quicksort algorithm on a Comparable list
      /// &lt;/summary&gt;
      /// &lt;param name="list"&gt;The list to sort&lt;/param&gt;
      public static void QuicksortParallel&lt;T&gt;(this List&lt;T&gt; list) where T : IComparable
      {
          if (list.Count &lt; 2048)
              list.QuicksortSequential();
          else
          {
              var info = new ThreadInfo(list.Count);
              list.QuicksortRecurseP(0, list.Count - 1, 0, info);
          }
      }
  
      /// &lt;summary&gt;
      /// Method to implement parallel quicksort recursion on a Comparable list
      /// &lt;/summary&gt;
      /// &lt;param name="list"&gt;The list to sort&lt;/param&gt;
      /// &lt;param name="start"&gt;The starting index of the partition&lt;/param&gt;
      /// &lt;param name="end"&gt;The ending index of the partition (inclusive)&lt;/param&gt;
      /// &lt;param name="depth"&gt;The current recursive depth&lt;/param&gt;
      /// &lt;param name="info"&gt;Structure holding decision-making info for threads&lt;/param&gt;
      private static void QuicksortRecurseP&lt;T&gt;(this List&lt;T&gt; list, int start, int end, int depth,
                                               ThreadInfo info)
                                               where T : IComparable
      {
          if (start &gt;= end)
              return;
  
          int middle = list.Partition(start, end);
  
          if (depth &lt; info.threadDepth)
          {
              var t = Task.Run(() =&gt;
              {
                  list.QuicksortRecurseP(start, middle - 1, depth + 1, info);
              });
  
              list.QuicksortRecurseP(middle + 1, end, depth + 1, info);
              t.Wait();
          }
          else
          {
              list.QuicksortRecurseS(start, middle - 1);
              list.QuicksortRecurseS(middle + 1, end);
          }
      }
  
      /// &lt;summary&gt;
      /// Method to begin sequential quicksort algorithm on a Comparable list
      /// &lt;/summary&gt;
      /// &lt;param name="list"&gt;The list to sort&lt;/param&gt;
      public static void QuicksortSequential&lt;T&gt;(this List&lt;T&gt; list) where T : IComparable
      {
          list.QuicksortRecurseS(0, list.Count - 1);
      }
  
      /// &lt;summary&gt;
      /// Method to implement sequential quicksort recursion on a Comparable list
      /// &lt;/summary&gt;
      /// &lt;param name="list"&gt;The list to sort&lt;/param&gt;
      /// &lt;param name="start"&gt;The starting index of the partition&lt;/param&gt;
      /// &lt;param name="end"&gt;The ending index of the partition (inclusive)&lt;/param&gt;
      private static void QuicksortRecurseS&lt;T&gt;(this List&lt;T&gt; list, int start, int end) where T : IComparable
      {
          if (start &gt;= end)
              return;
  
          int middle = list.Partition(start, end);
  
          // Now recursively sort the (approximate) halves.
          list.QuicksortRecurseS(start, middle - 1);
          list.QuicksortRecurseS(middle + 1, end);
      }
  }
  </code></pre>
  
  <p>As far as I understand, this methodology should produce a one-time startup cost, then proceed in sorting the rest of the data significantly faster than sequential method. However, the parallel method takes significantly more time than the sequential method, which actually increases as the load increases. Benchmarked at a list of ten million items on a 4-core CPU, the sequential method averages around 18 seconds to run to completion, while the parallel method takes upwards of 26 seconds. Increasing the allowed thread depth rapidly exacerbates the problem.</p>
  
  <p>Any help in finding the performance hog is appreciated. Thanks!</p>