From a16260c618cb3f164049d0eebf8d99c8e0fe01d9 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:29:41 +0900 Subject: [PATCH 1/7] perf: improve sort algorithm --- benchmarks/headers.mjs | 57 ++++++++ benchmarks/sort.mjs | 59 ++++++++ lib/fetch/headers.js | 114 +++++++++++++--- lib/fetch/sort.js | 190 ++++++++++++++++++++++++++ package.json | 2 +- test/fetch/headerslist-sortedarray.js | 39 ++++++ test/fetch/sort.js | 55 ++++++++ 7 files changed, 493 insertions(+), 23 deletions(-) create mode 100644 benchmarks/headers.mjs create mode 100644 benchmarks/sort.mjs create mode 100644 lib/fetch/sort.js create mode 100644 test/fetch/headerslist-sortedarray.js create mode 100644 test/fetch/sort.js diff --git a/benchmarks/headers.mjs b/benchmarks/headers.mjs new file mode 100644 index 00000000000..0484e6e1316 --- /dev/null +++ b/benchmarks/headers.mjs @@ -0,0 +1,57 @@ +import { bench, group, run } from 'mitata' +import { Headers } from '../lib/fetch/headers.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 settings = { + 'fast-path (tiny array)': 4, + 'fast-path (small array)': 8, + 'fast-path (middle array)': 16, + 'fast-path': 32, + 'slow-path': 64 +} + +for (const [name, length] of Object.entries(settings)) { + const headers = new Headers( + Array.from(Array(length), () => [generateAsciiString(12), '']) + ) + + const headersSorted = new Headers(headers) + + const kHeadersList = Reflect.ownKeys(headers).find( + (c) => String(c) === 'Symbol(headers list)' + ) + + const headersList = headers[kHeadersList] + + const headersListSorted = headersSorted[kHeadersList] + + const kHeadersSortedMap = Reflect.ownKeys(headersList).find( + (c) => String(c) === 'Symbol(headers map sorted)' + ) + + group(`length ${length} #${name}`, () => { + bench('Headers@@iterator', () => { + // prevention of memoization of results + headersList[kHeadersSortedMap] = null + return [...headers] + }) + + bench('Headers@@iterator (sorted)', () => { + // prevention of memoization of results + headersListSorted[kHeadersSortedMap] = null + return [...headersSorted] + }) + }) +} + +await run() diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs new file mode 100644 index 00000000000..e154767998a --- /dev/null +++ b/benchmarks/sort.mjs @@ -0,0 +1,59 @@ +import { bench, group, run } from 'mitata' +import { sort, binaryInsertionSort, heapSort, introSort } from '../lib/fetch/sort.js' +// import { sort as timSort } from './tim-sort.mjs' + +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 +} + +const settings = { + tiny: 32, + small: 64, + middle: 128, + large: 512 +} + +for (const [name, length] of Object.entries(settings)) { + group(`sort (${name})`, () => { + const array = Array.from(new Array(length), () => generateAsciiString(12)) + // sort(array, compare) + bench('Array#sort', () => array.slice().sort(compare)) + bench('sort (mixed sort)', () => sort(array.slice(), compare)) + // bench('tim sort', () => timSort(array.slice(), compare, 0, array.length)) + + // 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)) + + // Do not run them in large arrays as they are slow. + if (array.length <= 1000) bench('binary insertion sort', () => binaryInsertionSort(array.slice(), 0, array.length, compare)) + }) + + group(`sort sortedArray (${name})`, () => { + const array = Array.from(new Array(length), () => generateAsciiString(12)).sort(compare) + // sort(array, compare) + bench('Array#sort', () => array.sort(compare)) + bench('sort (mixed sort)', () => sort(array, compare)) + // bench('tim sort', () => timSort(array, compare, 0, array.length)) + + // sort(array, start, end, compare) + bench('intro sort', () => introSort(array, 0, array.length, compare)) + bench('heap sort', () => heapSort(array, 0, array.length, compare)) + + // Do not run them in large arrays as they are slow. + if (array.length <= 1000) bench('binary insertion sort', () => binaryInsertionSort(array, 0, array.length, compare)) + }) +} + +await run() diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 41ae9b02368..8ee66fbf996 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -12,6 +12,7 @@ const { } = require('./util') const { webidl } = require('./webidl') const assert = require('node:assert') +const { sort } = require('./sort') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') @@ -120,6 +121,10 @@ function appendHeader (headers, name, value) { // privileged no-CORS request headers from headers } +function compareHeaderName (a, b) { + return a[0] < b[0] ? -1 : 1 +} + class HeadersList { /** @type {[string, string][]|null} */ cookies = null @@ -237,7 +242,7 @@ class HeadersList { * [Symbol.iterator] () { // use the lowercased name - for (const [name, { value }] of this[kHeadersMap]) { + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { yield [name, value] } } @@ -253,6 +258,79 @@ class HeadersList { return headers } + + // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set + toSortedArray () { + const size = this[kHeadersMap].size + const array = new Array(size) + // In most cases, you will use the fast-path. + // fast-path: Use binary insertion sort for small arrays. + if (size <= 32) { + if (size === 0) { + // If empty, it is an empty array. To avoid the first index assignment. + return array + } + // Improve performance by unrolling loop and avoiding double-loop. + // Double-loop-less version of the binary insertion sort. + const iterator = this[kHeadersMap][Symbol.iterator]() + const firstValue = iterator.next().value + // set [name, value] to first index. + array[0] = [firstValue[0], firstValue[1].value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(firstValue[1].value !== null) + for ( + let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value; + i < size; + ++i + ) { + // get next value + value = iterator.next().value + // set [name, value] to current index. + x = array[i] = [value[0], value[1].value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(x[1] !== null) + left = 0 + right = i + // binary search + while (left < right) { + // middle index + pivot = left + ((right - left) >> 1) + // compare header name + if (array[pivot][0] <= x[0]) { + left = pivot + 1 + } else { + right = pivot + } + } + if (i !== pivot) { + j = i + while (j > left) { + array[j] = array[--j] + } + array[left] = x + } + } + /* c8 ignore next 4 */ + if (!iterator.next().done) { + // This is for debugging and will never be called. + throw new TypeError('Unreachable') + } + return array + } else { + // This case would be a rare occurrence. + // slow-path: fallback + let i = 0 + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { + array[i++] = [name, value] + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + // 3.2.2. Assert: value is non-null. + assert(value !== null) + } + return sort(array, compareHeaderName) + } + } } // https://fetch.spec.whatwg.org/#headers-class @@ -454,27 +532,19 @@ 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 = this[kHeadersList].toSortedArray() const cookies = this[kHeadersList].cookies + // fast-path + if (cookies === null) { + // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray` + return (this[kHeadersList][kHeadersSortedMap] = names) + } + // 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 @@ -491,17 +561,15 @@ class Headers { // 1. Let value be the result of getting name from list. // 2. Assert: value is non-null. - assert(value !== null) + // Note: This operation was done by `HeadersList#toSortedArray`. // 3. Append (name, value) to headers. headers.push([name, value]) } } - this[kHeadersList][kHeadersSortedMap] = headers - // 4. Return headers. - return headers + return (this[kHeadersList][kHeadersSortedMap] = headers) } [Symbol.for('nodejs.util.inspect.custom')] () { @@ -546,6 +614,8 @@ webidl.converters.HeadersInit = function (V) { module.exports = { fill, + // for test. + compareHeaderName, Headers, HeadersList } diff --git a/lib/fetch/sort.js b/lib/fetch/sort.js new file mode 100644 index 00000000000..d7ee2eca5dd --- /dev/null +++ b/lib/fetch/sort.js @@ -0,0 +1,190 @@ +'use strict' + +/** **binary insertion sort** + * - Best -> O(n) + * - Average -> O(n^2) + * - Worst -> O(n^2) + * - Memory -> O(n) total, O(1) auxiliary + * - Stable -> true + * @param {any[]} array + * @param {number} begin begin + * @param {number} end end + * @param {(a: any, b: any) => number} compare + */ +function binaryInsertionSort (array, begin, end, compare) { + for ( + let i = begin + 1, j = 0, right = 0, left = 0, pivot = 0, x; + i < end; + ++i + ) { + x = array[i] + left = 0 + right = i + // binary search + while (left < right) { + // middle index + pivot = left + ((right - left) >> 1) + if (compare(array[pivot], x) <= 0) { + left = pivot + 1 + } else { + right = pivot + } + } + if (i !== pivot) { + j = i + while (j > left) { + array[j] = array[--j] + } + array[left] = x + } + } + return array +} + +/** + * @param {number} num + */ +function log2 (num) { + // Math.floor(Math.log2(n)) + let log = 0 + // eslint-disable-next-line no-cond-assign + while ((num >>= 1)) ++log + return log +} + +/** **intro sort** + * - Average -> O(n log n) + * - Worst -> O(n log n) + * - Stable -> false + * @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) { + return binaryInsertionSort(array, begin, end, compare) + } + if (depth-- <= 0) { + return heapSort(array, begin, end, compare) + } + // median of three quick sort + let i = begin + let j = end - 1 + const pivot = medianOf3( + 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 + } + if (i - begin > 1) _introSort(array, begin, i, depth, compare) + // if (end - (j + 1) > 1) ... + if (end - j > 2) _introSort(array, j + 1, end, depth, compare) + return array +} + +/** **heap sort (bottom up)** + * - Best -> Ω(n) + * - Average -> O(n log n) + * - Worst -> O(n log n) + * - Memory -> O(n) total, O(1) auxiliary + * - Stable -> false + * @param {any[]} array + * @param {number} begin + * @param {number} end + * @param {(a: any, b: any) => number} compare + */ +function heapSort (array, begin, end, compare) { + const N = end - begin + let p = N >> 1 + let q = N - 1 + let x + while (p > 0) { + downHeap(array, array[begin + p - 1], begin, --p, q, compare) + } + while (q > 0) { + x = array[begin + q] + array[begin + q] = array[begin] + downHeap(array, (array[begin] = x), begin, 0, --q, compare) + } + return array +} + +/** + * @param {any[]} array + * @param {any} x + * @param {number} begin + * @param {number} p + * @param {number} q + * @param {(a: any, b: any) => number} compare + */ +function downHeap (array, x, begin, p, q, compare) { + let c + while ((c = (p << 1) + 1) <= q) { + if (c < q && compare(array[begin + c], array[begin + c + 1]) < 0) ++c + if (compare(x, array[begin + c]) >= 0) break + array[begin + p] = array[begin + c] + p = c + } + array[begin + p] = x +} + +/** + * @param {any} x + * @param {any} y + * @param {any} z + * @param {(a: any, b: any) => number} compare + */ +function medianOf3 (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) { + const length = array.length + if (length <= 128) { + return _introSort(array, 0, length, log2(length) << 1, compare) + } + // For sorted arrays, intro sort is slow, so use the native implementation. + // TODO: fix performance regression for sorted arrays. + return array.sort(compare) +} + +module.exports = { + sort, + binaryInsertionSort, + introSort, + heapSort +} diff --git a/package.json b/package.json index c85ec55102f..fabb4d5f1ba 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "jest": "^29.0.2", "jsdom": "^24.0.0", "jsfuzz": "^1.0.15", - "mitata": "^0.1.8", + "mitata": "^0.1.10", "node-fetch": "^3.3.2", "pre-commit": "^1.2.2", "proxy": "^1.0.2", diff --git a/test/fetch/headerslist-sortedarray.js b/test/fetch/headerslist-sortedarray.js new file mode 100644 index 00000000000..8647ab6e7af --- /dev/null +++ b/test/fetch/headerslist-sortedarray.js @@ -0,0 +1,39 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { HeadersList, compareHeaderName } = require('../../lib/fetch/headers') + +const characters = 'abcdefghijklmnopqrstuvwxyz0123456789' +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 SORT_RUN = 4000 + +test('toSortedArray (fast-path)', () => { + for (let i = 0; i < SORT_RUN; ++i) { + const headersList = new HeadersList() + for (let j = 0; j < 32; ++j) { + headersList.append(generateAsciiString(4), generateAsciiString(4)) + } + assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + } +}) + +// Do we really need it? +test('toSortedArray (slow-path)', () => { + for (let i = 0; i < SORT_RUN; ++i) { + const headersList = new HeadersList() + for (let j = 0; j < 64; ++j) { + headersList.append(generateAsciiString(4), generateAsciiString(4)) + } + assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + } +}) diff --git a/test/fetch/sort.js b/test/fetch/sort.js new file mode 100644 index 00000000000..90e2bc74085 --- /dev/null +++ b/test/fetch/sort.js @@ -0,0 +1,55 @@ +'use strict' + +const { describe, test } = require('node:test') +const assert = require('node:assert') +const { sort, heapSort, binaryInsertionSort, introSort } = require('../../lib/fetch/sort') + +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 + + const SORT_RUN = 4000 + + const SORT_ELEMENT = 200 + + const arrays = new Array(SORT_RUN) + const expectedArrays = new Array(SORT_RUN) + + for (let i = 0; i < SORT_RUN; ++i) { + const array = generateRandomNumberArray(SORT_ELEMENT) + const expected = array.slice().sort(compare) + arrays[i] = array + expectedArrays[i] = expected + } + + test('binary insertion sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('heap sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('intro sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) + } + }) +}) From e451c6aaf924c0ebaa61c6b98a01a85a855d42b2 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:45:57 +0900 Subject: [PATCH 2/7] benchmark: add headers-length32.mjs --- benchmarks/headers-length32.mjs | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 benchmarks/headers-length32.mjs diff --git a/benchmarks/headers-length32.mjs b/benchmarks/headers-length32.mjs new file mode 100644 index 00000000000..4a490276baa --- /dev/null +++ b/benchmarks/headers-length32.mjs @@ -0,0 +1,53 @@ +import { bench, run } from 'mitata' +import { Headers } from '../lib/fetch/headers.js' +import symbols from '../lib/core/symbols.js' + +const headers = new Headers( + [ + 'Origin-Agent-Cluster', + 'RTT', + 'Accept-CH-Lifetime', + 'X-Frame-Options', + 'Sec-CH-UA-Platform-Version', + 'Digest', + 'Cache-Control', + 'Sec-CH-UA-Platform', + 'If-Range', + 'SourceMap', + 'Strict-Transport-Security', + 'Want-Digest', + 'Cross-Origin-Resource-Policy', + 'Width', + 'Accept-CH', + 'Via', + 'Set-Cookie', + 'Server', + 'Sec-Fetch-Dest', + 'Sec-CH-UA-Model', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'Date', + 'Expires', + 'DNT', + 'Proxy-Authorization', + 'Alt-Svc', + 'Alt-Used', + 'ETag', + 'Sec-Fetch-User', + 'Sec-CH-UA-Full-Version-List', + 'Referrer-Policy' + ].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() From c6976c9c449918026b89e55622d2a7941570d978 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:53:29 +0900 Subject: [PATCH 3/7] fix: benchmark --- benchmarks/headers-length32.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/benchmarks/headers-length32.mjs b/benchmarks/headers-length32.mjs index 4a490276baa..d057ed77eeb 100644 --- a/benchmarks/headers-length32.mjs +++ b/benchmarks/headers-length32.mjs @@ -1,6 +1,5 @@ import { bench, run } from 'mitata' import { Headers } from '../lib/fetch/headers.js' -import symbols from '../lib/core/symbols.js' const headers = new Headers( [ @@ -20,7 +19,7 @@ const headers = new Headers( 'Width', 'Accept-CH', 'Via', - 'Set-Cookie', + 'Refresh', 'Server', 'Sec-Fetch-Dest', 'Sec-CH-UA-Model', @@ -39,7 +38,11 @@ const headers = new Headers( ].map((v) => [v, '']) ) -const headersList = headers[symbols.kHeadersList] +const kHeadersList = Reflect.ownKeys(headers).find( + (c) => String(c) === 'Symbol(headers list)' +) + +const headersList = headers[kHeadersList] const kHeadersSortedMap = Reflect.ownKeys(headersList).find( (c) => String(c) === 'Symbol(headers map sorted)' From 2f64ee1819cce9d0bb9755e8472fec810654d38c Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:40:30 +0900 Subject: [PATCH 4/7] fix: fix performance regression for sorted arrays --- benchmarks/sort.mjs | 14 +++----------- lib/fetch/sort.js | 15 ++++++--------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs index e154767998a..25ee90f670e 100644 --- a/benchmarks/sort.mjs +++ b/benchmarks/sort.mjs @@ -1,6 +1,5 @@ import { bench, group, run } from 'mitata' -import { sort, binaryInsertionSort, heapSort, introSort } from '../lib/fetch/sort.js' -// import { sort as timSort } from './tim-sort.mjs' +import { sort, heapSort, introSort } from '../lib/fetch/sort.js' function compare (a, b) { return a < b ? -1 : 1 @@ -29,30 +28,23 @@ for (const [name, length] of Object.entries(settings)) { const array = Array.from(new Array(length), () => generateAsciiString(12)) // sort(array, compare) bench('Array#sort', () => array.slice().sort(compare)) - bench('sort (mixed sort)', () => sort(array.slice(), compare)) - // bench('tim sort', () => timSort(array.slice(), compare, 0, array.length)) + bench('sort (intro sort)', () => sort(array.slice(), 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)) - - // Do not run them in large arrays as they are slow. - if (array.length <= 1000) bench('binary insertion sort', () => binaryInsertionSort(array.slice(), 0, array.length, compare)) }) group(`sort sortedArray (${name})`, () => { const array = Array.from(new Array(length), () => generateAsciiString(12)).sort(compare) // sort(array, compare) bench('Array#sort', () => array.sort(compare)) - bench('sort (mixed sort)', () => sort(array, compare)) + bench('sort (intro sort)', () => sort(array, compare)) // bench('tim sort', () => timSort(array, compare, 0, array.length)) // sort(array, start, end, compare) bench('intro sort', () => introSort(array, 0, array.length, compare)) bench('heap sort', () => heapSort(array, 0, array.length, compare)) - - // Do not run them in large arrays as they are slow. - if (array.length <= 1000) bench('binary insertion sort', () => binaryInsertionSort(array, 0, array.length, compare)) }) } diff --git a/lib/fetch/sort.js b/lib/fetch/sort.js index d7ee2eca5dd..230f2e2645c 100644 --- a/lib/fetch/sort.js +++ b/lib/fetch/sort.js @@ -45,7 +45,7 @@ function binaryInsertionSort (array, begin, end, compare) { * @param {number} num */ function log2 (num) { - // Math.floor(Math.log2(n)) + // Math.floor(Math.log2(num)) let log = 0 // eslint-disable-next-line no-cond-assign while ((num >>= 1)) ++log @@ -88,6 +88,7 @@ function _introSort (array, begin, end, depth, compare) { array[j], compare ) + let firstPass = true while (true) { while (compare(array[i], pivot) < 0) ++i while (compare(pivot, array[j]) < 0) --j @@ -95,10 +96,11 @@ function _introSort (array, begin, end, depth, compare) { [array[i], array[j]] = [array[j], array[i]] ++i --j + firstPass = false } - if (i - begin > 1) _introSort(array, begin, i, depth, compare) + if (i - begin > 1 && !firstPass) _introSort(array, begin, i, depth, compare) // if (end - (j + 1) > 1) ... - if (end - j > 2) _introSort(array, j + 1, end, depth, compare) + if (end - j > 2 && !firstPass) _introSort(array, j + 1, end, depth, compare) return array } @@ -174,12 +176,7 @@ function medianOf3 (x, y, z, compare) { */ function sort (array, compare) { const length = array.length - if (length <= 128) { - return _introSort(array, 0, length, log2(length) << 1, compare) - } - // For sorted arrays, intro sort is slow, so use the native implementation. - // TODO: fix performance regression for sorted arrays. - return array.sort(compare) + return _introSort(array, 0, length, log2(length) << 1, compare) } module.exports = { From d20832f8e5b5ee9fe898279dd72c73286bb62649 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:46:00 +0900 Subject: [PATCH 5/7] test: add sorted test --- test/fetch/sort.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/fetch/sort.js b/test/fetch/sort.js index 90e2bc74085..4ea7762d8c1 100644 --- a/test/fetch/sort.js +++ b/test/fetch/sort.js @@ -53,3 +53,44 @@ describe('sort', () => { } }) }) + +describe('sorted', () => { + const compare = (a, b) => a - b + + const SORT_RUN = 4000 + + const SORT_ELEMENT = 200 + + const arrays = new Array(SORT_RUN) + const expectedArrays = new Array(SORT_RUN) + + for (let i = 0; i < SORT_RUN; ++i) { + const array = generateRandomNumberArray(SORT_ELEMENT).sort(compare) + arrays[i] = array + expectedArrays[i] = array.slice() + } + + test('binary insertion sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(binaryInsertionSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('heap sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(heapSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('intro sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(introSort(arrays[i].slice(), 0, SORT_ELEMENT, compare), expectedArrays[i]) + } + }) + + test('sort', () => { + for (let i = 0; i < SORT_RUN; ++i) { + assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) + } + }) +}) \ No newline at end of file From 41e1e9a97d4e401b956577d79944f482843c7b95 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:47:05 +0900 Subject: [PATCH 6/7] refactor: simplify --- test/fetch/sort.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/fetch/sort.js b/test/fetch/sort.js index 4ea7762d8c1..a373c2a62ef 100644 --- a/test/fetch/sort.js +++ b/test/fetch/sort.js @@ -12,13 +12,13 @@ function generateRandomNumberArray (length) { return array } -describe('sort', () => { - const compare = (a, b) => a - b +const compare = (a, b) => a - b - const SORT_RUN = 4000 +const SORT_RUN = 4000 - const SORT_ELEMENT = 200 +const SORT_ELEMENT = 200 +describe('sort', () => { const arrays = new Array(SORT_RUN) const expectedArrays = new Array(SORT_RUN) @@ -55,12 +55,6 @@ describe('sort', () => { }) describe('sorted', () => { - const compare = (a, b) => a - b - - const SORT_RUN = 4000 - - const SORT_ELEMENT = 200 - const arrays = new Array(SORT_RUN) const expectedArrays = new Array(SORT_RUN) @@ -93,4 +87,4 @@ describe('sorted', () => { assert.deepStrictEqual(sort(arrays[i].slice(), compare), expectedArrays[i]) } }) -}) \ No newline at end of file +}) From 40ee52c904962bb6a7763741d0e042471f1adb60 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:54:49 +0900 Subject: [PATCH 7/7] refactor: remove comment --- benchmarks/sort.mjs | 1 - test/fetch/headerslist-sortedarray.js | 1 - 2 files changed, 2 deletions(-) diff --git a/benchmarks/sort.mjs b/benchmarks/sort.mjs index 25ee90f670e..a1c413a00de 100644 --- a/benchmarks/sort.mjs +++ b/benchmarks/sort.mjs @@ -40,7 +40,6 @@ for (const [name, length] of Object.entries(settings)) { // sort(array, compare) bench('Array#sort', () => array.sort(compare)) bench('sort (intro sort)', () => sort(array, compare)) - // bench('tim sort', () => timSort(array, compare, 0, array.length)) // sort(array, start, end, compare) bench('intro sort', () => introSort(array, 0, array.length, compare)) diff --git a/test/fetch/headerslist-sortedarray.js b/test/fetch/headerslist-sortedarray.js index 8647ab6e7af..72112f92288 100644 --- a/test/fetch/headerslist-sortedarray.js +++ b/test/fetch/headerslist-sortedarray.js @@ -27,7 +27,6 @@ test('toSortedArray (fast-path)', () => { } }) -// Do we really need it? test('toSortedArray (slow-path)', () => { for (let i = 0; i < SORT_RUN; ++i) { const headersList = new HeadersList()