23

I've implemented a mergesort and a quicksort to compare them with the native JavaScript sort. For the quicksort I've tried to use this algorithm: view algorithm on youtube. Both algorithms use as less memory as possible, for the merge sort an auxiliary array is passed for each recursive call (to avoid overheads) , and for the quicksort, positions of the starting and ending positions. I'm using sorts to manage large amounts of data in a NodeJs app.

Below you have the mergesort, quicksort and native JavaScript sort and you can test the performance

The question is: Why is the native JavaScript performing slower ?

In my case:

Chrome - merge sort: measure: 1997.920ms; quicksort: measure: 1755.740ms; native : measure: 4988.105ms
Node: merge sort: measure: 2233.413ms; quicksort: measure: 1876.055ms; native: measure: 6317.118ms

Merge Sort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array) {
  function merge(arr, aux, lo, mid, hi) {
    for (var k = lo; k <= hi; k++) {
      aux[k] = arr[k];
    }

    var i = lo;
    var j = mid + 1;
    for (var k = lo; k <= hi; k++) {
      if (i > mid) {
        arr[k] = aux[j++];
      } else if (j > hi) {
        arr[k] = aux[i++];
      } else if (aux[i] < aux[j]) {
        arr[k] = aux[i++];
      } else {
        arr[k] = aux[j++];
      }
    }
  }

  function sort(array, aux, lo, hi) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sort(array, aux, lo, mid);
    sort(array, aux, mid + 1, hi);

    merge(array, aux, lo, mid, hi);
  }

  function merge_sort(array) {
    var aux = array.slice(0);
    sort(array, aux, 0, array.length - 1);
    return array;
  }

  return merge_sort(array);
}


console.time('measure');
mergeSort(arr);
console.timeEnd('measure');
console.log(arr[0], arr[1]);

Quicksort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}

function quickSort(arr, leftPos, rightPos, arrLength) {
  let initialLeftPos = leftPos;
  let initialRightPos = rightPos;
  let direction = true;
  let pivot = rightPos;
  while ((leftPos - rightPos) < 0) {
    if (direction) {
      if (arr[pivot] < arr[leftPos]) {
        quickSort.swap(arr, pivot, leftPos);
        pivot = leftPos;
        rightPos--;
        direction = !direction;
      } else
        leftPos++;
    } else {
      if (arr[pivot] <= arr[rightPos]) {
        rightPos--;
      } else {
        quickSort.swap(arr, pivot, rightPos);
        leftPos++;
        pivot = rightPos;
        direction = !direction;
      }
    }
  }
  if (pivot - 1 > initialLeftPos) {
    quickSort(arr, initialLeftPos, pivot - 1, arrLength);
  }
  if (pivot + 1 < initialRightPos) {
    quickSort(arr, pivot + 1, initialRightPos, arrLength);
  }
}
quickSort.swap = (arr, el1, el2) => {
  let swapedElem = arr[el1];
  arr[el1] = arr[el2];
  arr[el2] = swapedElem;
}
arrLength = arr.length;
console.time('measure');
quickSort(arr, 0, arrLength - 1, arrLength);
console.log(arr[0], arr[1]);
console.timeEnd('measure');

Native Javascript Sort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 100000000));
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  return a - b;
});
console.timeEnd('measure');

console.log(arr[0], arr[1]);
Relu Mesaros
  • 4,952
  • 4
  • 25
  • 33
  • FYI in the order you wrote them, the first is the slowest the second the fastest considering my PC and the randomness :) – Lucio Aug 03 '16 at 00:28
  • yes, Quicksort performs the fastest .. so native js performs better than merge sort on your pc ? – Relu Mesaros Aug 03 '16 at 00:30
  • Interesting. I checked these out in Firefox and Edge. Not as much difference between the three of them in Firefox, though native sort was still the slowest. In Edge, the first one never finishes or perhaps I gave up too soon. bit it appeared to never finish. The last two completed in Edge. – jfriend00 Aug 03 '16 at 00:31
  • @jfriend00 Same here. I can confirm that Edge doesn't like merge sort... – Arnauld Aug 03 '16 at 00:39
  • I find both of your sorts twice as slow as native - Firefox – Jaromanda X Aug 03 '16 at 00:42
  • try them in Chrome of NodeJS – Relu Mesaros Aug 03 '16 at 00:45
  • Chrome - merge sort: measure: 1997.920ms; quicksort: measure: 1755.740ms; native : measure: 4988.105ms – Relu Mesaros Aug 03 '16 at 00:47
  • Node: merge sort: measure: 2233.413ms; quicksort: measure: 1876.055ms; native: measure: 6317.118ms – Relu Mesaros Aug 03 '16 at 00:49
  • @ReluMesaros Note each test case has a different array, and depending on how the array is defined can greatly affect the run time of them. – Spencer Wieczorek Aug 03 '16 at 00:49
  • I know, but the average is still among the same values – Relu Mesaros Aug 03 '16 at 00:50
  • Edge seems to just be really, really slow with the merge sort. Dropping one zero off the length and it takes 2 seconds which at N(log N), it would take a LOT longer to do the 10x more of the full length you show here. – jfriend00 Aug 03 '16 at 00:51
  • @jfriend00 I guess Edge isn't very efficient working with a large amount of memory. – Spencer Wieczorek Aug 03 '16 at 00:58
  • 3
    What if you pass comparator into your custom implementations as an argument? The 2 custom implementations have hard coded order, while the native is general purpose. – zerkms Aug 03 '16 at 01:02
  • 4
    very well might have something to do with the fact that vanilla sort is flexible and is not able to take advantage of the native comparison operators. I'd be interested to see what the times look like if you rewrote mergeSort and quickSort to use compareNumbers rather than `<`, `>`, or `==` – Hamms Aug 03 '16 at 01:03
  • "*`quickSort.__proto__.swap = …`*" - don't do that. If at all, you want `quickSort.swap = …`. – Bergi Aug 03 '16 at 02:05
  • You can speed up the merge sort by removing the loop to copy arr into aux. Instead use two mutually recursive functions: merge sort arr to arr, and merge sort arr to aux. The entry function is merge sort arr to arr, which calls merge sort arr to aux twice, once for the left half, once for the right half, then merges the two halves from aux back to arr. Merge sort arr to aux calls merge sort arr to arr twice, then merges from arr to aux, or if size == 1, it copies one element from arr to aux. You could also use a boolean to keep track of the merge direction with a single recursive function. – rcgldr Aug 03 '16 at 02:16
  • @zerkms can you provide/show me a hardcoded example that will perform much faster on the native js than on the merge sort ? – Relu Mesaros Aug 03 '16 at 02:20
  • @ReluMesaros you cannot do that - since by contract the built-in method accepts a comparator. What I suggested is to change your code to be fairer - so that your implementations also supported comparators. – zerkms Aug 03 '16 at 02:24
  • thanx @Bergi, i forgot it there, initially I was trying something else; – Relu Mesaros Aug 03 '16 at 02:27
  • @rcgldr, I'll try it out, I'm curios how the performance is affected – Relu Mesaros Aug 03 '16 at 02:28
  • @ReluMesaros - I added an answer with the optimized top down merge sort. – rcgldr Aug 03 '16 at 04:02
  • 7724.563ms, 39297.258ms, 35142.367ms respectively, in my slow laptop – caub Aug 03 '16 at 08:03
  • @zerkms you can look at the bellow comment provided by rcgldr (Merge Sort with compare function). In his example you can pass a comparator into the custom implementation and is still performing better than native js sort – Relu Mesaros Aug 03 '16 at 15:21
  • @ReluMesaros sure I can, since I participated in his answer comments :-D – zerkms Aug 03 '16 at 21:08
  • @zerkms, haa, yees, sorry, my mind is in the outer space sometimes or most of times :D – Relu Mesaros Aug 03 '16 at 21:22

2 Answers2

18

So why is the native sort slower? Looking at the code in

https://github.com/v8/v8/blob/0c76b0ae850027006d5ec0d92449e449d996d3bb/src/js/array.js#L744

The issue seems to be GetThirdIndex(). It gets called when partition size is > 1000, and I assume it's used to prevent quicksort worst case performance, but the overhead is significant, as it creates internal arrays of pairs and sorts those, and the sorting of those pairs can result in further recursive calls to GetThirdIndex(). This is combined with the recursive calls related to partitioning the original array and to partitioning the internal arrays of pairs.

Since the test data for these examples is random data, Relu's quicksort doesn't need something like GetThirdIndex(). There's also the check for "holes" in the array, but I assume this isn't significant.

An alternative to GetThirdIndex() would be an in place median of medians:

http://en.wikipedia.org/wiki/Median_of_medians

Merge sort is faster than quicksort with these methods used to prevent worst case scenarios, but it needs an auxiliary array the same size or half the size of the original array.

Introsort, which is a hybrid of quicksort and heapsort, switching to heapsort if the level of recursion gets too deep would be an alternative:

http://en.wikipedia.org/wiki/Introsort

The second merge sort example below uses a compare function, to make a fair comparison. It's significantly faster than the native version. In the case of Chrome, a compare function didn't affect the overall time much. In the case of Firefox, the compare function had more effect. In the case of Firefox, the native version failed for out of memory so I couldn't compare that.

These are somewhat faster versions of top down merge sort which the original poster was "curious" about, using mutually recursive functions to avoid copy and somewhat optimized merge() (two conditionals per compare).

Results with Firefox (times vary somewhat)

native sort - failed for out of memory.
Relu's merge sort - 1.8 seconds
Relu's quick sort - 1.3 seconds
optimized merge sort - 1.4 seconds
optimized merge sort with compare - 1.8 seconds

Results with Chrome (times vary somewhat)

native sort - 5.3 seconds
Relu's merge sort - 2.1 seconds
Relu's quick sort - 1.8 seconds
optimized merge sort - 1.6 seconds
optimized merge sort with compare - 1.7 seconds

Merge Sort

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array) {
  function merge(arr, aux, lo, mid, hi) {
    var i = lo;
    var j = mid + 1;
    var k = lo;
    while(true){
      if(arr[i] <= arr[j]){
        aux[k++] = arr[i++];
        if(i > mid){
          do
            aux[k++] = arr[j++];
          while(j <= hi);
          break;
        }
      } else {
        aux[k++] = arr[j++];
        if(j > hi){
          do
            aux[k++] = arr[i++];
          while(i <= mid);
          break;
        }
      }
    }
  }

  function sortarrtoaux(arr, aux, lo, hi) {
    if (hi < lo) return;
    if (hi == lo){
        aux[lo] = arr[lo];
        return;
    }
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoarr(arr, aux, lo, mid);
    sortarrtoarr(arr, aux, mid + 1, hi);
    merge(arr, aux, lo, mid, hi);
  }

  function sortarrtoarr(arr, aux, lo, hi) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoaux(arr, aux, lo, mid);
    sortarrtoaux(arr, aux, mid + 1, hi);
    merge(aux, arr, lo, mid, hi);
  }

  function merge_sort(arr) {
    var aux = arr.slice(0);
    sortarrtoarr(arr, aux, 0, arr.length - 1);
    return arr;
  }

  return merge_sort(array);
}

console.time('measure');
mergeSort(arr);
console.timeEnd('measure');
console.log(arr[0], arr[1]);

Merge Sort with compare function

var length = 10000000; //  ten millions;
var arr = [];
for (let i = length; i > 0; i--) {
  // random array
  arr.push(parseInt(Math.random() * 1000000000));
}
var mergeSort = function(array, comparefn) {
  function merge(arr, aux, lo, mid, hi, comparefn) {
    var i = lo;
    var j = mid + 1;
    var k = lo;
    while(true){
      var cmp = comparefn(arr[i], arr[j]);
      if(cmp <= 0){
        aux[k++] = arr[i++];
        if(i > mid){
          do
            aux[k++] = arr[j++];
          while(j <= hi);
          break;
        }
      } else {
        aux[k++] = arr[j++];
        if(j > hi){
          do
            aux[k++] = arr[i++];
          while(i <= mid);
          break;
        }
      }
    }
  }

  function sortarrtoaux(arr, aux, lo, hi, comparefn) {
    if (hi < lo) return;
    if (hi == lo){
        aux[lo] = arr[lo];
        return;
    }
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoarr(arr, aux, lo, mid, comparefn);
    sortarrtoarr(arr, aux, mid + 1, hi, comparefn);
    merge(arr, aux, lo, mid, hi, comparefn);
  }

  function sortarrtoarr(arr, aux, lo, hi, comparefn) {
    if (hi <= lo) return;
    var mid = Math.floor(lo + (hi - lo) / 2);
    sortarrtoaux(arr, aux, lo, mid, comparefn);
    sortarrtoaux(arr, aux, mid + 1, hi, comparefn);
    merge(aux, arr, lo, mid, hi, comparefn);
  }

  function merge_sort(arr, comparefn) {
    var aux = arr.slice(0);
    sortarrtoarr(arr, aux, 0, arr.length - 1, comparefn);
    return arr;
  }

  return merge_sort(array, comparefn);
}

console.time('measure');
mergeSort(arr, function compareNumbers(a, b) {
  return a - b;
});
console.timeEnd('measure');
// check result
for (let i = 1; i < length; i++) {
    if(arr[i] < arr[i-1]){
        console.log('error');
        break;
    }
}
console.log(arr[0], arr[1]);

Side note: native sort is not stable:

Native Javascript Sort - test for stability

var length = 100000;
var arr = [];
var j;
for (let i = 0; i < length; i++) {
  j = parseInt(Math.random() * 100);
  arr[i] = [j, i];
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  return a[0] - b[0];
});
console.timeEnd('measure');

for (let i = 1; i < length; i++) {
    if( (arr[i][0] == arr[i-1][0]) &&
        (arr[i][1] <  arr[i-1][1]) ){
        console.log('not stable');
        console.log(arr[i-1][0], arr[i-1][1]);
        console.log(arr[i  ][0], arr[i  ][1]);
        break;
    }
}

Native Javascript Sort - change compare to make it stable

var length = 100000;
var arr = [];
var j;
for (let i = 0; i < length; i++) {
  j = parseInt(Math.random() * 100);
  arr[i] = [j, i];
}

console.time('measure');
arr.sort(function compareNumbers(a, b) {
  if(a[0] == b[0])
    return a[1] - b[1];
  return a[0] - b[0];
});
console.timeEnd('measure');

for (let i = 1; i < length; i++) {
    if( (arr[i][0] == arr[i-1][0]) &&
        (arr[i][1] <  arr[i-1][1]) ){
        console.log('not stable');
        console.log(arr[i-1][0], arr[i-1][1]);
        console.log(arr[i  ][0], arr[i  ][1]);
        break;
    }
}
Community
  • 1
  • 1
rcgldr
  • 27,407
  • 3
  • 36
  • 61
  • "I'm wondering if arr.sort() is converting numbers to be objects, despite having a compare function that should work with numbers." --- why not check? https://github.com/v8/v8/blob/master/src/js/array.js#L712 – zerkms Aug 03 '16 at 04:12
  • @rcgldr no, I mean - you're making assumptions about how V8 is implemented instead of checking it. "The conversion could result in a level of indirection (like pointers in C) that would increase the sort time." --- this statement makes no sense, since see the link above. – zerkms Aug 03 '16 at 04:18
  • @zerkms - I added a merge sort with compare function to my answer, and it's still significantly faster than the native sort. – rcgldr Aug 03 '16 at 05:30
  • @rcgldr yep, that's because comparator function is definitely inlined there. I did not expect to see any difference, and just suggested it for the implementations to be in fair conditions. – zerkms Aug 03 '16 at 05:30
  • 3
    @zzzzBov - I updated my answer to include a plausible reason for why native sort is slow. – rcgldr Aug 03 '16 at 06:20
  • 1
    This flaw has existed for years, from Google, the company that notoriously makes interviewees regurgitate sort algorithms. Not sure if this is more ironic or pathetic. – Dirigible Jan 19 '18 at 19:02
  • An answer on [How to sort an array of integers correctly](//stackoverflow.com/a/53355543) suggests a TypedArray of `Uint32Array` as being 5x faster than standard `sort()` with a comparison function. Other types can be used depending on your data. Also, a deleted answer suggests LSD Radix Sort from [hpc-algorithms](https://www.npmjs.com/package/hpc-algorithms) npm, https://github.com/DragonSpit/hpc-algorithms. – Peter Cordes Nov 20 '19 at 05:26
0

I do not know how 14 is greater than 76 and 61 enter image description here enter image description here enter image description here

JS Disciple
  • 318
  • 1
  • 7