JS源碼解析之Array.prototype.sort

前言

今天有個小夥伴( chrome v59 )遇到一個這樣的問題,html

[1,2,13,14,5,6,17,18,9,10,11,12,31,41].sort(()=>0)
// [18,1,13,14,5,6,17,2,9,10,11,12,31,41]
[1,2,13,14,5,6,17,18,9,10].sort(()=>0)
// [1,2,13,14,5,6,17,18,9,10]
複製代碼

而後我在本身電腦上( chrome v76 )測試是這樣的結果前端

[1,2,13,14,5,6,17,18,9,10,11,12,31,41].sort(()=>0)
// [1,2,13,14,5,6,17,18,9,10,11,12,31,41]
[1,2,13,14,5,6,17,18,9,10].sort(()=>0)
// [1,2,13,14,5,6,17,18,9,10]
複製代碼

咱們知道,給一個 sort 的比較函數中返回0,表示當前比較的兩個元素相等git

照理說,sort(()=>0) 後數組的元素順序是不變的,和個人測試效果一致,github

那爲何在 低版本的 chrome 上,不一樣長度的數組運用 sort(()=>0) 後效果不同呢?算法

定義

arr.sort([compareFunction])
複製代碼

這裏咱們引用MDN的一段話:chrome

若是 compareFunction(a, b) 小於 0 ,那麼 a 會被排列到 b 以前;數組

若是 compareFunction(a, b) 大於 0 , b 會被排列到 a 以前。瀏覽器

若是 compareFunction(a, b) 等於 0 , a 和 b 的相對位置不變。備註: ECMAScript 標準並不保證這一行爲,並且也不是全部瀏覽器都會遵照(例如 Mozilla 在 2003 年以前的版本)bash

也就是說,有些瀏覽器不遵循 compareFunction(a, b) 等於 0時, a 和 b 的相對位置不變 的規則函數

這裏咱們看出來了,chrome v59 就是不遵循該規則的。 可是 數組長度較小時好像又遵循了?

這裏咱們猜想不一樣長度的數組會運用不一樣的排序算法

在分析源碼以前,咱們先簡單提下,什麼是 插入排序 和 快速排序

排序算法

咱們假設比較函數爲

comparefn = (a,b)=> a-b
複製代碼

1. 插入排序

遍歷數組,將每一個待排序元素插入到前面已排序的適當位置

插入排序分爲 直接插入排序、二分查找插入排序、希爾排序

因爲v8也只是用了直接插入排序,這裏咱們只實現它,其餘幾種不進行討論,想要了解的能夠參考這裏,

function InsertionSort(array) {
  for (let i = 1; i < array.legnth; i++) {
    let element = array[i];
    // 將待排序元素element插入對應位置
    for (let j = i - 1; j >= 0; j--) {
      let tmp = array[j];
      // comparefn > 0 表示element要排在tmp以前
      if (comparefn(tmp, element) > 0) {
        a[j + 1] = tmp;
      } else {
        //
        break;
      }
    }
    a[j + 1] = element;
  }
};
複製代碼

2. 快速排序

設定一個基準,利用該基準值大小將數組分爲左右兩部分

此時左右兩部分能夠獨立排序,分別對左右兩部分進行上面的操做

遞歸處理,直至數組排序完成

考慮到空間消耗,如今的快速排序通常都是指原地算法的快速排序

關於原地算法,參看 en.wikipedia.org/wiki/In-pla…

下面有二者實現,基準值取左邊的或者右邊,效果差很少

function qsort(array){
  function swap(arr,i1,i2){
    let tmp = arr[i1]
    arr[i1] = arr[i2]
    arr[i2] = tmp
  }
  function partition(arr, left, right){
    let storeIndex = left
    let pivot = arr[right] //基準
    for(let i=left;i<right;i++){
      if(arr[i]<pivot){
        swap(arr,storeIndex++,i)
      }
    }
    swap(arr,storeIndex,right)
    return storeIndex
  }
  // 基準在左邊
  // function partition(arr, left, right){
  // let storeIndex = left
  // let pivot = arr[left] //基準
  // for(let i = left+1;i<=right;i++){
  // if(arr[i]<pivot){
  // swap(arr,++storeIndex,i)
  // }
  // }
  // swap(arr,storeIndex,left)
  // return storeIndex
  // }
  function sort(arr,left,right){
    if(left<right){
      let storeIndex = partition(arr, left, right);
      sort(arr, left, storeIndex - 1);
      sort(arr, storeIndex + 1, right);
    }
  }
  sort(array, 0, array.length - 1);
  return array
}
複製代碼

v8 源碼分析

理解了基本的排序算法,接下來咱們開始研究源碼。

比較 chrome v59 和 chrome v76 的 v8 實現差別在哪

如何查找對應chrome版本的 v8 源碼

打開chrome://version/

上面顯示的 JavaScript 便是 v8 的版本

Google Chrome	76.0.3809.132 (正式版本) (64 位) (cohort: Stable)
操做系統	Windows 10 OS Version 1809 (Build 17763.316)
JavaScript	V8 7.6.303.29
複製代碼

也正如 V8’s version numbering scheme 所述

Chromium 76 對應 v8 的 7.6

接着咱們直接去 v8 查看源碼,這裏主要看兩個版本的

5.9.221

對應的排序算法 源碼地址

結合測試用例看更佳 /test/mjsunit/array-sort

能夠看出來,早期v8 排序的實現邏輯是用js寫的,對應的實現爲 ArraySort

utils.InstallFunctions(GlobalArray.prototype, DONT_ENUM, [
  ...
  "sort", getFunction("sort", ArraySort),
  ...
])
複製代碼
  • ArraySort

沒有什麼有用代碼,直接進入 InnerArraySort

function ArraySort(comparefn) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.sort");

  var array = TO_OBJECT(this);
  var length = TO_LENGTH(array.length);
  return InnerArraySort(array, length, comparefn);
}
複製代碼
  • InnerArraySort

對類數組對象以及空洞數組進行特殊處理,而後進行排序

// comparefn 不可調用(未定義,非function等),設置默認函數
if (!IS_CALLABLE(comparefn)) {
  comparefn = function (x, y) {
    if (x === y) return 0;
    if (% _IsSmi(x) && % _IsSmi(y)) {
      return % SmiLexicographicCompare(x, y);
    }
    x = TO_STRING(x);
    y = TO_STRING(y);
    if (x == y) return 0;
    else return x < y ? -1 : 1;
  };
}
if (length < 2) return array;

var is_array = IS_ARRAY(array);
var max_prototype_element;
if (!is_array) {
  // 對 類數組對象(好比 {length:10,0:'c',10:'b'}) 進行排序,兼容 JSC標準
  // 考慮了繼承屬性,因此效率可能不高,不過這種須要排序的狀況較少
  // e.g. 也能夠看這個例子 https://github.com/v8/v8/blob/5.9.221/test/mjsunit/array-sort.js#L337
  /* let f1 = {1: "c", 3: "f"} let f2 = {6: "a", length: 10} f2.__proto__ = f1 f2 // {6: "a", length: 10,__proto__:{1: "c", 3: "f"}} Array.prototype.sort.call(f2) // {0: "a", 1: "b", 2: "c", 3: "f", length: 10} */
  // 返回自身及原型鏈中全部屬性的個數
  max_prototype_element = CopyFromPrototype(array, length);
}
// 快速RemoveArrayHoles:從數組末尾複製已定義元素填充到前面的空洞(末尾變爲空洞)
// 類數組對象等狀況不支持快速RemoveArrayHoles,會返回 -1
// 不然 返回已定義元素的個數
var num_non_undefined = % RemoveArrayHoles(array, length);
// 處理類數組對象等狀況
if (num_non_undefined == -1) {
  // 返回 類數組對象的已定義實例屬性的個數
  num_non_undefined = SafeRemoveArrayHoles(array);
}

QuickSort(array, 0, num_non_undefined);

if (!is_array && (num_non_undefined + 1 < max_prototype_element)) {
  // 處理 原型同名屬性 等狀況
  ShadowPrototypeElements(array, num_non_undefined, max_prototype_element);
}

return array;
複製代碼

其餘的特殊處理不在文本論述中,咱們直接看排序實現

  • QuickSort
function QuickSort (a, from, to) {
  // 基準選擇第一個元素
  var third_index = 0;
  while (true) {
    // 待排序數組長度 <= 10 採用插入排序
    if (to - from <= 10) {
      InsertionSort(a, from, to);
      return;
    }
    if (to - from > 1000) {
      // 每隔 200 ~ 215 (根據 length & 15的結果)個元素取一個值,
      // 而後將這些值進行排序,取中間值的下標
      // 這裏的排序其實又是一個遞歸調用
      third_index = GetThirdIndex(a, from, to);
    } else {
      // 將中間元素設爲基準值
      third_index = from + ((to - from) >> 1);
    }
    // 將第一個,中間元素(上面獲取的基準值),最後一個元素三者中的中位數做爲基準值
    var v0 = a[from];
    var v1 = a[to - 1];
    var v2 = a[third_index];
    var c01 = comparefn(v0, v1);
    if (c01 > 0) {
      // v1 < v0, so swap them.
      var tmp = v0;
      v0 = v1;
      v1 = tmp;
    } // v0 <= v1.
    var c02 = comparefn(v0, v2);
    if (c02 >= 0) {
      // v2 <= v0 <= v1.
      var tmp = v0;
      v0 = v2;
      v2 = v1;
      v1 = tmp;
    } else {
      // v0 <= v1 && v0 < v2
      var c12 = comparefn(v1, v2);
      if (c12 > 0) {
        // v0 <= v2 < v1
        var tmp = v1;
        v1 = v2;
        v2 = tmp;
      }
    }
    // 最終效果 v0 <= v1 <= v2
    a[from] = v0;
    a[to - 1] = v2;
    var pivot = v1;
    var low_end = from + 1;   // 比基準值小的元素的上界
    var high_start = to - 1;  // 比基準值大的元素的下界
    // 將基準值與 from + 1 位置的元素進行互換
    // 此時 from + 1 位置的元素確定是要排 form 位置後面的
    a[third_index] = a[low_end];
    a[low_end] = pivot;

    // 劃分函數 將小於(假設升序排序)基準值的元素排在左邊
    partition: for (var i = low_end + 1; i < high_start; i++) {
      var element = a[i];
      var order = comparefn(element, pivot);
      if (order < 0) {
        a[i] = a[low_end];
        a[low_end] = element;
        low_end++;
      } else if (order > 0) {
        // 當待排序元素大於基準值時,
        // 與到右側第一個小於基準值的元素互換
        do {
          high_start--;
          if (high_start == i) break partition;
          var top_elem = a[high_start];
          order = comparefn(top_elem, pivot);
        } while (order > 0);
        a[i] = a[high_start];
        a[high_start] = element;
        // 該元素小於基準值,須要排在基準值左邊
        if (order < 0) {
          element = a[i];
          a[i] = a[low_end];
          a[low_end] = element;
          low_end++;
        }
      }
    }
    // 對左右兩個子數組再進行排序
    // 先處理待排序元素較少的
    if (to - high_start < low_end - from) {
      QuickSort(a, high_start, to);
      to = low_end;
    } else {
      QuickSort(a, from, low_end);
      from = high_start;
    }
  }
};
複製代碼
  • [1,2,13,14,5,6,17,18,9,10,11,12,31,14,51]comparefn = (a,b)=>a-b 爲例
  1. third_index = 7, from = 0, to = 15, pivot = a[7] = 18, low_end = 1, high_start = 14, a[7] = a[1] = 2, a[1] = 18

[1,18,13,14,5,6,17,2,9,10,11,12,31,14,51]

  1. 進入 partition 循環,從 i=2開始比較
  2. i=2,因爲 a[i] < pivot, 此時 a[2] = a[low_end] = a[1] = 18, low_end = 2

[1,13,18,14,5,6,17,2,9,10,11,12,31,14,51]

  1. 直到 i=12 纔出現 a[i]=31 > pivot, 這段過程結束後 low_end = 11

[1,13,14,5,6,17,2,9,10,11,12,18,31,14,51]

  1. i=12,因爲 a[12] = 31 > pivot,high_start = 13,因爲 a[13] < pivot,a[i=12]=a[13]=14,a[13]=31

[1,13,14,5,6,17,2,9,10,11,12,18,14,31,51]

  1. 同時因爲 a[13] < pivot,a[12] = a[low_end] = a[11] = 18,a[11] = 31, low_end = 12

[1,13,14,5,6,17,2,9,10,11,12,14,18,31,51]

  1. i < high_start 不成立,循環中斷
  2. to - high_start = 14-13=1,low_end - from = 12,故先進行 QuickSort(a,13,14) 再進入循環判斷 QuickSort(a,0,12)
  • [1,2,13,14,5,6,17,18,9,10,11,12,31,14,51]comparefn = (a,b)=>0 爲例
  1. third_index = 7, from = 0, to = 15, v0=a[0]=1,v[1]=a[14]=51,v[2]=a[7]=18,
  2. 因爲 comparefn(v0, v2)>=0,表示 v2 <= v0 <= v1,v0=18,v1=1,v2=51, a[0]=18,a[14]=51,pivot = v1 = 1, low_end = 1, high_start = 14, a[7] = a[1] = 2, a[1] = 1

[18,1,13,14,5,6,17,2,9,10,11,12,31,14,51]

  1. 進入 partition 循環,從 i=2開始比較,因爲 comparefn(element, pivot)=0 不進行處理直至循環結束
  2. to - high_start = 14-14=0,low_end - from = 0,故先進行 QuickSort(a,0,0) 再進入循環判斷 QuickSort(a,14,14)
  3. 判斷結束,返回 [18,1,13,14,5,6,17,2,9,10,11,12,31,14,51]

能夠看出來,v8源碼有兩個問題

1、v0,v1,v2 的交換處理代碼

comparefn = (a,b)=>0
function swap ([v0, v1, v2]) {
  // 給定 v0,v1,v2
  // 對其進行排序,保證 v0<=v1<=v2
  var c01 = comparefn(v0, v1);
  if (c01 > 0) {
    // v1 < v0, so swap them.
    var tmp = v0;
    v0 = v1;
    v1 = tmp;
  }
  // 此時 v0 <= v1.
  var c02 = comparefn(v0, v2);
  if (c02 > 0) {
    // v2 < v0 <= v1.的狀況 進行交換
    var tmp = v0;
    v0 = v2;
    v2 = v1;
    v1 = tmp;
  } else {
    // v0 <= v1 && v0 <= v2
    var c12 = comparefn(v1, v2);
    if (c12 > 0) {
      // v1 > v2
      var tmp = v1;
      v1 = v2;
      v2 = tmp;
    }
  }
  return [v0, v1, v2]
}

複製代碼

主要是 c02 的判斷上改成 > ,保證 v0與v2相同時 不會進行交換

2、從新賦值

原來代碼在交換後,作了這些操做,沒有考慮相等的狀況

a[from] = v0;
a[to - 1] = v2;
var pivot = v1;
var low_end = from + 1;   // Upper bound of elements lower than pivot.
var high_start = to - 1;  // Lower bound of elements greater than pivot.
a[third_index] = a[low_end];
a[low_end] = pivot;
複製代碼

假設 v0,v1,v2的順序不變,可是原來 a[to-1] 的值是v1 此時變成v2,故在一開始賦值時應該變動順序

var v0 = a[from];
var v1 = a[third_index];
var v2 = a[to - 1];
複製代碼

a[third_index] 是否與 a[low_end] 交換,也應該作個判斷

if(comparefn(pivot,a[low_end])!==0){
  a[third_index] = a[low_end];
  a[low_end] = pivot;
} else {
  a[third_index] = pivot
}
複製代碼

優化後的快排函數

function ArraySort (array, comparefn) {
  function InsertionSort (a, from, to) {
    for (var i = from + 1; i < to; i++) {
      var element = a[i];
      for (var j = i - 1; j >= from; j--) {
        var tmp = a[j];
        var order = comparefn(tmp, element);
        if (order > 0) {
          a[j + 1] = tmp;
        } else {
          break;
        }
      }
      a[j + 1] = element;
    }
  };

  function GetThirdIndex (a, from, to) {
    var t_array = new Array();
    // Use both 'from' and 'to' to determine the pivot candidates.
    var increment = 200 + ((to - from) & 15);
    var j = 0;
    from += 1;
    to -= 1;
    for (var i = from; i < to; i += increment) {
      t_array[j] = [i, a[i]];
      j++;
    }
    t_array.sort(function (a, b) {
      return comparefn(a[1], b[1]);
    });
    var third_index = t_array[t_array.length >> 1][0];
    return third_index;
  }
  function QuickSort (a, from, to) {
    // 基準選擇第一個元素
    var third_index = 0;
    while (true) {
      // 待排序數組長度 <= 10 採用插入排序
      if (to - from <= 10) {
        InsertionSort(a, from, to);
        return;
      }
      if (to - from > 1000) {
        // 每隔 200 ~ 215 (根據 length & 15的結果)個元素取一個值,
        // 而後將這些值進行排序,取中間值的下標
        // 這裏的排序其實又是一個遞歸調用
        third_index = GetThirdIndex(a, from, to);
      } else {
        // 將中間元素設爲基準值
        third_index = from + ((to - from) >> 1);
      }
      // 將第一個,中間元素(上面獲取的基準值),最後一個元素三者中的中位數做爲基準值
      var v0 = a[from];
      var v1 = a[third_index];
      var v2 = a[to - 1];
      var c01 = comparefn(v0, v1);
      if (c01 > 0) {
        // v1 < v0, so swap them.
        var tmp = v0;
        v0 = v1;
        v1 = tmp;
      }
      // 此時 v0 <= v1.
      var c02 = comparefn(v0, v2);
      if (c02 > 0) {
        // v2 < v0 <= v1.的狀況 進行交換
        var tmp = v0;
        v0 = v2;
        v2 = v1;
        v1 = tmp;
      } else {
        // v0 <= v1 && v0 <= v2
        var c12 = comparefn(v1, v2);
        if (c12 > 0) {
          // v1 > v2
          var tmp = v1;
          v1 = v2;
          v2 = tmp;
        }
      }
      // 最終效果 v0 <= v1 <= v2
      a[from] = v0;
      a[to - 1] = v2;
      var pivot = v1;
      var low_end = from + 1;   // 比基準值小的元素的上界
      var high_start = to - 1;  // 比基準值大的元素的下界
      // 將基準值與 from + 1 位置的元素進行互換
      // 此時 from + 1 位置的元素確定是要排 form 位置後面的
      if (comparefn(pivot, a[low_end]) !== 0) {
        a[third_index] = a[low_end];
        a[low_end] = pivot;
      } else {
        a[third_index] = pivot
      }

      // 劃分函數 將小於(假設升序排序)基準值的元素排在左邊
      partition: for (var i = low_end + 1; i < high_start; i++) {
        var element = a[i];
        var order = comparefn(element, pivot);
        if (order < 0) {
          a[i] = a[low_end];
          a[low_end] = element;
          low_end++;
        } else if (order > 0) {
          // 當待排序元素大於基準值時,
          // 與到右側第一個小於基準值的元素互換
          do {
            high_start--;
            if (high_start == i) break partition;
            var top_elem = a[high_start];
            order = comparefn(top_elem, pivot);
          } while (order > 0);
          a[i] = a[high_start];
          a[high_start] = element;
          // 該元素小於基準值,須要排在基準值左邊
          if (order < 0) {
            element = a[i];
            a[i] = a[low_end];
            a[low_end] = element;
            low_end++;
          }
        }
      }
      // 對左右兩個子數組再進行排序
      // 先處理待排序元素較少的
      if (to - high_start < low_end - from) {
        QuickSort(a, high_start, to);
        to = low_end;
      } else {
        QuickSort(a, from, low_end);
        from = high_start;
      }
    }
  };
  QuickSort(array, 0, array.length)
  return array
}
ArraySort([1,2,13,14,5,6,17,18,9,10,11,12,31,41],()=>0)
//  [1, 2, 13, 14, 5, 6, 17, 18, 9, 10, 11, 12, 31, 41]
ArraySort([1,2,13,14,5,6,17,18,9,10,11,12,31,41],(a,b)=>a-b)
//  [1, 2, 5, 6, 9, 10, 11, 12, 13, 14, 17, 18, 31, 41]
複製代碼

7.6.303

根據 V8引擎中的排序 得知,在v8 的7.0版本中修改了 Array.prototype.sort 的實現,再也不採用js實現,進而採用一直叫 Torque 的語言,相似 TypeScript,強類型。

v8 中的 src/js/array.js 在大概 7.2以後的版本刪除,中間幾個版本用來遷移 array的其餘方法

源碼路徑 /third_party/v8/builtins/array-sort.tq

能夠得知,sort 更換了實現,採用了 TimSort 排序算法

簡單的說:

  1. 掃描數組,肯定其中的單調上升段和嚴格單調降低段,將嚴格降低段反轉。咱們將這樣的段稱之爲run。
  2. 定義最小run長度,短於此的run經過插入排序合併直至長度大於最小run長度;
  3. 反覆歸併一些相鄰run,過程當中須要避免歸併長度相差很大的run,直至整個排序完成;

實現仍是較爲複雜的,本文不進行深刻,具體的能夠查看 TimSort的實現 一文

其餘

在看源碼的時候又發現一個實現差別的問題

chrome v59

[1,,2,,3,4,5].sort(v=>0)
// [1, 5, 2, 4, 3, undefined x 2]
複製代碼

chrome 76

[1,,2,,3,4,5].sort(v=>0)
// [1, 2, 3, 4, 5, empty × 2]
複製代碼

新版的實現應該是較爲科學的

還有一些有趣的差別能夠看這裏

總結

低版本 v8 的快排實現有bug,當數組較小時採用插入排序是沒問題的

新版本的chrome所使用的v8版本實現了穩定排序,並解決了一些潛在問題(與開發者想要的實現效果不一樣)

最後分享一個 V8源碼中尋找JS方法實現 的技巧

  1. 根據部分文件名快速查找文件:help.github.com/en/articles…
  2. 在搜索欄中輸入 "Array.prototype.sort" in:file 便可搜索含有全匹配 Array.prototype.sort 內容的文件
  3. js方法實現通常在 /src/js/ 和 /src/runtime/ 目錄中 www.zhihu.com/question/59…
  4. 結合 test/mjsunit 一塊兒看源碼效果更佳

參考

  1. es5中文規範
  2. MDN-Array.prototype.sort
  3. 【深度】扒開V8引擎的源碼,我找到了大家想要的前端算法
  4. JavaScript專題之解讀 v8 排序源碼
  5. Getting things sorted in V8
相關文章
相關標籤/搜索