Skip to content

Commit

Permalink
perf: improve sort algorithm to improve performance for large arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
tsctx committed Feb 17, 2024
1 parent d3128c1 commit 90aa558
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 18 deletions.
31 changes: 31 additions & 0 deletions benchmarks/headers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { bench, run } from 'mitata'
import { Headers } from '../lib/fetch/headers.js'
import symbols from '../lib/core/symbols.js'

const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length

function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}

const headers = new Headers(
Array.from(Array(100), () => generateAsciiString(7)).map((v) => [v, ''])
)

const headersList = headers[symbols.kHeadersList]

const kHeadersSortedMap = Reflect.ownKeys(headersList).find(
(c) => String(c) === 'Symbol(headers map sorted)'
)

bench('Headers@@iterator', () => {
headersList[kHeadersSortedMap] = null
return [...headers]
})

await run()
43 changes: 43 additions & 0 deletions benchmarks/sort.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { bench, group, run } from 'mitata'
import { sort, binaryInsertionSort, heapSort, introSort } from '../lib/fetch/util.js'

function compare (a, b) {
return a < b ? -1 : 1
}

const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length

function generateAsciiString (length) {
let result = ''
for (let i = 0; i < length; ++i) {
result += characters[Math.floor(Math.random() * charactersLength)]
}
return result
}

function insertionSort (list, compare) {
for (let i = 1, j = 0, l = list.length, value; i < l; ++i) {
value = list[i]
for (j = i - 1; j >= 0; --j) {
if (compare(list[j], value) <= 0) break
list[j + 1] = list[j]
}
list[j + 1] = value
}
return list
}

group('sort', () => {
const array = new Array(20).fill(null).map(() => generateAsciiString(10))
// sort(array, compare)
bench('sort', () => sort(array.slice(), compare))
bench('insertion sort', () => insertionSort(array.slice(), compare))
bench('binary insertion sort', () => binaryInsertionSort(array.slice(), compare))
bench('native', () => array.slice().sort(compare))
// sort(array, start, end, compare)
bench('intro sort', () => introSort(array.slice(), 0, array.length, compare))
bench('heap sort', () => heapSort(array.slice(), 0, array.length, compare))
})

await run()
31 changes: 13 additions & 18 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const { kEnumerableProperty } = require('../core/util')
const {
iteratorMixin,
isValidHeaderName,
isValidHeaderValue
isValidHeaderValue,
headerNamesSort
} = require('./util')
const { webidl } = require('./webidl')
const assert = require('node:assert')
Expand Down Expand Up @@ -253,6 +254,14 @@ class HeadersList {

return headers
}

toArray () {
const result = []
for (const { 0: name, 1: { value } } of this[kHeadersMap]) {
result.push([name, value])
}
return result
}
}

// https://fetch.spec.whatwg.org/#headers-class
Expand Down Expand Up @@ -454,27 +463,13 @@ class Headers {

// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = [...this[kHeadersList]]
const namesLength = names.length
if (namesLength <= 16) {
// Note: Use insertion sort for small arrays.
for (let i = 1, value, j = 0; i < namesLength; ++i) {
value = names[i]
for (j = i - 1; j >= 0; --j) {
if (names[j][0] <= value[0]) break
names[j + 1] = names[j]
}
names[j + 1] = value
}
} else {
names.sort((a, b) => a[0] < b[0] ? -1 : 1)
}
const names = headerNamesSort(this[kHeadersList].toArray())

const cookies = this[kHeadersList].cookies

// 3. For each name of names:
for (let i = 0; i < namesLength; ++i) {
const [name, value] = names[i]
for (let i = 0; i < names.length; ++i) {
const { 0: name, 1: value } = names[i]
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
Expand Down
157 changes: 157 additions & 0 deletions lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,158 @@ function getDecodeSplit (name, list) {
return gettingDecodingSplitting(value)
}

/**
* @param {any[]} array
* @param {(a: any, b: any) => number} compare
*/
function binaryInsertionSort (array, compare) {
for (let i = 1, j = 0, l = array.length, right = 0, left = 0, pivotMid = 0, x; i < l; ++i) {
x = array[i]
left = 0
right = i
while (left < right) {
pivotMid = left + ((right - left) >> 1)
// array[pivotMid] <= x
if (compare(array[pivotMid], x) <= 0) {
left = pivotMid + 1
} else {
right = pivotMid
}
}
if (i !== pivotMid) {
for (j = i; j > left; --j) {
array[j] = array[j - 1]
}
array[left] = x
}
}
return array
}

/**
* @param {number} num
*/
function log2 (num) {
let log = 0
// eslint-disable-next-line no-cond-assign
while (num >>= 1) ++log
return log
}

/**
* @param {any[]} array
* @param {number} begin begin
* @param {number} end end
* @param {(a: any, b: any) => number} compare
*/
function introSort (array, begin, end, compare) {
return _introSort(array, begin, end, log2(end - begin) << 1, compare)
}

/**
* @param {any[]} array
* @param {number} begin
* @param {number} end
* @param {number} depth
* @param {(a: any, b: any) => number} compare
*/
function _introSort (array, begin, end, depth, compare) {
if (end - begin <= 32) {
for (let i = begin + 1, j = 0, right = 0, left = 0, pivot = 0, x; i < end; ++i) {
x = array[i]
left = 0
right = i
while (left < right) {
pivot = left + ((right - left) >> 1)
if (compare(array[pivot], x) <= 0) {
left = pivot + 1
} else {
right = pivot
}
}
if (i !== pivot) {
for (j = i; j > left; --j) {
array[j] = array[j - 1]
}
array[left] = x
}
}
return array
}
if (depth-- <= 0) {
return heapSort(array, begin, end, compare)
}
let i = begin; let j = end - 1
const pivot = med3(array[i], array[i + ((j - i) >> 1)], array[j], compare)
while (true) {
while (compare(array[i], pivot) < 0) ++i
while (compare(pivot, array[j]) < 0) --j
if (i >= j) break;
[array[i], array[j]] = [array[j], array[i]]
++i; --j
}
++j
if (i - begin > 1) _introSort(array, begin, i, depth, compare)
if (end - j > 1) _introSort(array, j, end, depth, compare)
return array
}

function heapSort (array, begin, end, compare) {
const N = end - begin; let p = N >> 1; let q = N - 1
while (p > 0) {
downHeap(array, begin, --p, q, compare)
}
while (q > 0) {
[array[begin], array[begin + q]] = [array[begin + q], array[begin]]
downHeap(array, begin, 0, --q, compare)
}
return array
}

function downHeap (a, begin, p, q, compare) {
const tmp = a[begin + p]
let c
while ((c = (p << 1) + 1) <= q) {
if (c < q && compare(a[begin + c], a[begin + c + 1]) < 0) ++c
if (compare(tmp, a[begin + c]) >= 0) break
a[begin + p] = a[begin + c]; p = c
}
a[begin + p] = tmp
}

/**
* @param {any} x
* @param {any} y
* @param {any} z
* @param {(a: any, b: any) => number} compare
*/
function med3 (x, y, z, compare) {
return compare(x, y) < 0 ? compare(y, z) < 0 ? y : compare(z, x) < 0 ? x : z : compare(z, y) < 0 ? y : compare(x, z) < 0 ? x : z
}

/**
* @param {any[]} array
* @param {(a: any, b: any) => number} compare
*/
function sort (array, compare) {
if (array.length <= 32) {
return binaryInsertionSort(array, compare)
} else {
return introSort(array, 0, array.length, compare)
}
}

function compareHeaderName (a, b) {
return a[0] < b[0] ? -1 : 1
}

/**
* @param {[string, string][]} array
*/
function headerNamesSort (array) {
return sort(array, compareHeaderName)
}

module.exports = {
isAborted,
isCancelled,
Expand Down Expand Up @@ -1466,6 +1618,11 @@ module.exports = {
bytesMatch,
isReadableStreamLike,
readableStreamClose,
headerNamesSort,
sort,
binaryInsertionSort,
heapSort,
introSort,
isomorphicEncode,
urlIsLocal,
urlHasHttpsScheme,
Expand Down
41 changes: 41 additions & 0 deletions test/fetch/sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

const { describe, test } = require('node:test')
const assert = require('node:assert')
const { sort, heapSort, binaryInsertionSort } = require('../../lib/fetch/util')

function generateRandomNumberArray (length) {
const array = new Uint16Array(length)
for (let i = 0; i < length; ++i) {
array[i] = (65535 * Math.random()) | 0
}
return array
}

describe('sort', () => {
const compare = (a, b) => a - b

test('binary insertion sort', () => {
for (let i = 0; i <= 2000; ++i) {
const array = generateRandomNumberArray(100)
const expected = array.slice().sort(compare)
assert.deepStrictEqual(binaryInsertionSort(array, compare), expected)
}
})

test('heap sort', () => {
for (let i = 0; i <= 2000; ++i) {
const array = generateRandomNumberArray(100)
const expected = array.slice().sort(compare)
assert.deepStrictEqual(heapSort(array, 0, array.length, compare), expected)
}
})

test('intro sort', () => {
for (let i = 0; i <= 2000; ++i) {
const array = generateRandomNumberArray(100)
const expected = array.slice().sort(compare)
assert.deepStrictEqual(sort(array, compare), expected)
}
})
})

0 comments on commit 90aa558

Please sign in to comment.