From 1464eba1a0e8641ed710f859934613e9c1de53e1 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Sun, 22 Oct 2023 23:12:38 +0200 Subject: [PATCH] lib: improve performance of validateStringArray and validateBooleanArray PR-URL: https://github.com/nodejs/node/pull/49756 Reviewed-By: Yagiz Nizipli Reviewed-By: Trivikram Kamat --- benchmark/validators/parse-file-mode.js | 61 ++++++++++++++++++ benchmark/validators/validate-array.js | 68 ++++++++++++++++++++ benchmark/validators/validate-boolean.js | 55 ++++++++++++++++ benchmark/validators/validate-encoding.js | 59 ++++++++++++++++++ benchmark/validators/validate-one-of.js | 69 +++++++++++++++++++++ benchmark/validators/validate-x-array.js | 66 ++++++++++++++++++++ lib/internal/validators.js | 16 +++-- test/benchmark/test-benchmark-validators.js | 1 - 8 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 benchmark/validators/parse-file-mode.js create mode 100644 benchmark/validators/validate-array.js create mode 100644 benchmark/validators/validate-boolean.js create mode 100644 benchmark/validators/validate-encoding.js create mode 100644 benchmark/validators/validate-one-of.js create mode 100644 benchmark/validators/validate-x-array.js diff --git a/benchmark/validators/parse-file-mode.js b/benchmark/validators/parse-file-mode.js new file mode 100644 index 00000000000000..5e3819fc4229a9 --- /dev/null +++ b/benchmark/validators/parse-file-mode.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e7], + value: [ + "'777'", + '0o777', + ], +}, { + flags: ['--expose-internals'], +}); + +function getParseFactory() { + const { + parseFileMode, + } = require('internal/validators'); + + return (n) => parseFileMode(n, 'n'); +} + +function main({ n, value }) { + const parse = getParseFactory(); + + value = value === "'777'" ? '777' : 0o777; + + // Warm up. + const length = 1024; + const array = []; + let errCase = false; + + for (let i = 0; i < length; ++i) { + try { + array.push(parse(value)); + } catch (e) { + errCase = true; + array.push(e); + } + } + + bench.start(); + + for (let i = 0; i < n; ++i) { + const index = i % length; + try { + array[index] = parse(value); + } catch (e) { + array[index] = e; + } + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], errCase ? 'object' : 'number'); + } +} diff --git a/benchmark/validators/validate-array.js b/benchmark/validators/validate-array.js new file mode 100644 index 00000000000000..28f7b42102d198 --- /dev/null +++ b/benchmark/validators/validate-array.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e7], + value: [ + '[]', + '[1,2,3]', + ], +}, { + flags: ['--expose-internals'], +}); + +function getValidateFactory() { + const { + validateArray, + } = require('internal/validators'); + + return (n) => validateArray(n, 'n'); +} + +function main({ n, value }) { + const validate = getValidateFactory(); + + switch (value) { + case '[]': + value = []; + break; + case '[1,2,3]': + value = [1, 2, 3]; + break; + } + + // Warm up. + const length = 1024; + const array = []; + let errCase = false; + + for (let i = 0; i < length; ++i) { + try { + array.push(validate(value)); + } catch (e) { + errCase = true; + array.push(e); + } + } + + bench.start(); + + for (let i = 0; i < n; ++i) { + const index = i % length; + try { + array[index] = validate(value); + } catch (e) { + array[index] = e; + } + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], errCase ? 'object' : 'undefined'); + } +} diff --git a/benchmark/validators/validate-boolean.js b/benchmark/validators/validate-boolean.js new file mode 100644 index 00000000000000..24c2202b4fb743 --- /dev/null +++ b/benchmark/validators/validate-boolean.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e8], + code: [ + 'validateBoolean', + ], + value: [ + 'true', + 'false', + ], +}, { + flags: ['--expose-internals'], +}); + +function getValidateFactory(code) { + const { + validateBoolean, + } = require('internal/validators'); + + switch (code) { + case 'validateBoolean': + return (n) => validateBoolean(n, 'n'); + } +} + +function main({ n, code, value }) { + const validate = getValidateFactory(code); + const v = value === 'true'; + + // Warm up. + const length = 1024; + const array = []; + for (let i = 0; i < length; ++i) { + array.push(validate(v)); + } + + bench.start(); + + for (let i = 0; i < n; ++i) { + const index = i % length; + array[index] = validate(v); + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], 'undefined'); + } +} diff --git a/benchmark/validators/validate-encoding.js b/benchmark/validators/validate-encoding.js new file mode 100644 index 00000000000000..7bb6d8b0cba969 --- /dev/null +++ b/benchmark/validators/validate-encoding.js @@ -0,0 +1,59 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e8], + encoding: [ + 'ascii', + 'utf8', + 'utf-8', + 'utf16le', + 'ucs2', + 'ucs-2', + 'base64', + 'latin1', + 'binary', + 'hex', + ], + value: [ + 'test', + ], +}, { + flags: ['--expose-internals'], +}); + +function getValidateFactory(encoding) { + const { + validateEncoding, + } = require('internal/validators'); + + return (n) => validateEncoding(n, encoding); +} + +function main({ n, encoding, value }) { + const validate = getValidateFactory(encoding); + + // Warm up. + const length = 1024; + const array = []; + for (let i = 0; i < length; ++i) { + array.push(validate(value)); + } + + bench.start(); + + for (let i = 0; i < n; ++i) { + const index = i % length; + array[index] = validate(value); + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], 'undefined'); + } +} diff --git a/benchmark/validators/validate-one-of.js b/benchmark/validators/validate-one-of.js new file mode 100644 index 00000000000000..4812cb233e239b --- /dev/null +++ b/benchmark/validators/validate-one-of.js @@ -0,0 +1,69 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e7], + code: [ + 'validateOneOf', + ], + value: [ + 'fifo', + 'lifo', + 'lilo', + ], + validLength: [ + 1, + 2, + 3, + ], +}, { + flags: ['--expose-internals'], +}); + +const validValues = [ + 'fifo', + 'lifo', + 'lilo', + 'filo', +]; + +function getValidateFactory(code, validLength) { + const { + validateOneOf, + } = require('internal/validators'); + + switch (code) { + case 'validateOneOf': + return (n) => validateOneOf(n, 'n', validValues.slice(0, validLength)); + } +} + +function main({ n, code, validLength }) { + const validate = getValidateFactory(code, validLength); + + // Warm up. + const length = 1024; + const array = []; + + const value = validValues[validLength - 1]; + + for (let i = 0; i < length; ++i) { + array.push(validate(value)); + } + + bench.start(); + for (let i = 0; i < n; ++i) { + const index = i % length; + array[index] = validate(value); + } + bench.end(n); + + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], 'undefined'); + } +} diff --git a/benchmark/validators/validate-x-array.js b/benchmark/validators/validate-x-array.js new file mode 100644 index 00000000000000..aaef9119f35ae1 --- /dev/null +++ b/benchmark/validators/validate-x-array.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e8], + type: [ + 'validateStringArray', + 'validateBooleanArray', + ], + arrayLength: [ + 0, + 1, + 10, + 100, + ], +}, { + flags: ['--expose-internals'], +}); + +function getValidateFactory(type, arrayLength) { + const { + validateBooleanArray, + validateStringArray, + } = require('internal/validators'); + + switch (type) { + case 'validateBooleanArray': + return [ + (n) => validateBooleanArray(n, 'n'), + Array.from({ length: arrayLength }, (v, i) => ((i & 1) === 0)), + ]; + case 'validateStringArray': + return [ + (n) => validateStringArray(n, 'n'), + Array.from({ length: arrayLength }, (v, i) => `foo${i}`), + ]; + } +} + +function main({ n, type, arrayLength }) { + const [validate, value] = getValidateFactory(type, arrayLength); + + // Warm up. + const length = 1024; + const array = []; + for (let i = 0; i < length; ++i) { + array.push(validate(value)); + } + + bench.start(); + + for (let i = 0; i < n; ++i) { + const index = i % length; + array[index] = validate(value); + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], 'undefined'); + } +} diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 088a0a668ba8b2..77b67a4a37ff91 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -311,8 +311,12 @@ const validateArray = hideStackFrames((value, name, minLength = 0) => { /** @type {validateStringArray} */ function validateStringArray(value, name) { validateArray(value, name); - for (let i = 0; i < value.length; i++) { - validateString(value[i], `${name}[${i}]`); + for (let i = 0; i < value.length; ++i) { + // Don't use validateString here for performance reasons, as + // we would generate intermediate strings for the name. + if (typeof value[i] !== 'string') { + throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]); + } } } @@ -326,8 +330,12 @@ function validateStringArray(value, name) { /** @type {validateBooleanArray} */ function validateBooleanArray(value, name) { validateArray(value, name); - for (let i = 0; i < value.length; i++) { - validateBoolean(value[i], `${name}[${i}]`); + for (let i = 0; i < value.length; ++i) { + // Don't use validateBoolean here for performance reasons, as + // we would generate intermediate strings for the name. + if (value[i] !== true && value[i] !== false) { + throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'boolean', value[i]); + } } } diff --git a/test/benchmark/test-benchmark-validators.js b/test/benchmark/test-benchmark-validators.js index 3a6caba9b0bb20..37250f56588f51 100644 --- a/test/benchmark/test-benchmark-validators.js +++ b/test/benchmark/test-benchmark-validators.js @@ -4,7 +4,6 @@ require('../common'); // Minimal test for assert benchmarks. This makes sure the benchmarks aren't // completely broken but nothing more than that. - const runBenchmark = require('../common/benchmark'); runBenchmark('validators');