Replace isWhiteSpace with something faster

isWhiteSpace works by trimming the entire string. This works, but in the
context we use it, this is a lot of unnecessary work.
At this point we know that `+x === 0` and we want to know if that's
because x is empty or because it actually contains 0, hence we can use
a loop that just checks for a 0 existing in the string. This relies on
the assumption that there is no x such that:
+x === 0 && x.trim().length !== 0 && !x.includes('0')

In a very biased microbenchmark (https://scratch.mit.edu/projects/788352711)
this results in a 10% performance improvement.

This also will allow us to resolve https://github.com/TurboWarp/scratch-vm/issues/115
in a later commit.
This commit is contained in:
GarboMuffin committed 2023-01-14 22:06:26 -06:00
1 parent cb2eba9eb3
commit 4aa167e523
4 files changed
+68 -14

No files matched your search

+20 -11
View File
@@ -234,12 +234,21 @@ runtimeFunctions.limitPrecision = `const limitPrecision = value => {
}`;
/**
* Check if a value is considered whitespace.
* Similar to Cast.isWhiteSpace()
* @param {*} val Value to check
* @returns {boolean} true if the value is whitespace
* Used internally by the compare family of function.
* See similar method in cast.js.
* @param {*} val A value that evaluates to 0 in JS string-to-number conversation such as empty string, 0, or tab.
* @returns {boolean} True if the value should not be treated as the number zero.
*/
baseRuntime += `const isWhiteSpace = val => typeof val === 'string' && val.trim().length === 0;`;
baseRuntime += `const isNotActuallyZero = val => {
if (typeof val !== 'string') return false;
for (let i = 0; i < val.length; i++) {
const code = val.charCodeAt(i);
if (code === 48) {
return false;
}
}
return true;
};`;
/**
* Determine if two values are equal.
@@ -249,9 +258,9 @@ baseRuntime += `const isWhiteSpace = val => typeof val === 'string' && val.trim(
*/
baseRuntime += `const compareEqualSlow = (v1, v2) => {
const n1 = +v1;
if (isNaN(n1) || (n1 === 0 && isWhiteSpace(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
if (isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
const n2 = +v2;
if (isNaN(n2) || (n2 === 0 && isWhiteSpace(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
if (isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
return n1 === n2;
};
const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) && !isNaN(v2) || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`;
@@ -265,9 +274,9 @@ const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'numbe
runtimeFunctions.compareGreaterThan = `const compareGreaterThanSlow = (v1, v2) => {
let n1 = +v1;
let n2 = +v2;
if (n1 === 0 && isWhiteSpace(v1)) {
if (n1 === 0 && isNotActuallyZero(v1)) {
n1 = NaN;
} else if (n2 === 0 && isWhiteSpace(v2)) {
} else if (n2 === 0 && isNotActuallyZero(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
@@ -288,9 +297,9 @@ const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === '
runtimeFunctions.compareLessThan = `const compareLessThanSlow = (v1, v2) => {
let n1 = +v1;
let n2 = +v2;
if (n1 === 0 && isWhiteSpace(v1)) {
if (n1 === 0 && isNotActuallyZero(v1)) {
n1 = NaN;
} else if (n2 === 0 && isWhiteSpace(v2)) {
} else if (n2 === 0 && isNotActuallyZero(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
+18 -2
View File
@@ -11,6 +11,22 @@ const Color = require('../util/color');
* Use when coercing a value before computation.
*/
/**
* Used internally by compare()
* @param {*} val A value that evaluates to 0 in JS string-to-number conversation such as empty string, 0, or tab.
* @returns {boolean} True if the value should not be treated as the number zero.
*/
const isNotActuallyZero = val => {
if (typeof val !== 'string') return false;
for (let i = 0; i < val.length; i++) {
const code = val.charCodeAt(i);
if (code === 48) {
return false;
}
}
return true;
};
class Cast {
/**
* Scratch cast to number.
@@ -121,9 +137,9 @@ class Cast {
static compare (v1, v2) {
let n1 = Number(v1);
let n2 = Number(v2);
if (n1 === 0 && Cast.isWhiteSpace(v1)) {
if (n1 === 0 && isNotActuallyZero(v1)) {
n1 = NaN;
} else if (n2 === 0 && Cast.isWhiteSpace(v2)) {
} else if (n2 === 0 && isNotActuallyZero(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
+29
View File
@@ -0,0 +1,29 @@
const Cast = require('../../src/util/cast');
const {test} = require('tap');
test('Cast.compare with assorted whitespace characters', t => {
// The lack of tab characters in these tests is intentional.
t.equal(Cast.compare('', ''), 0);
t.equal(Cast.compare(' ', ''), 1);
t.equal(Cast.compare(' ', ' '), -1);
t.equal(Cast.compare(' \u00a0 ', '\r\n'), 1);
t.equal(Cast.compare(' 0', 0), 0);
t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\n'), 0);
t.equal(Cast.compare(' 0 ', ' \r\n\u00a0 0 \n\n\n\b'), 1);
t.equal(Cast.compare(' 0', '0'), 0);
t.equal(Cast.compare('', 0), -1);
t.equal(Cast.compare(' ', 0), -1);
t.equal(Cast.compare(' ', 0), -1);
t.equal(Cast.compare('0', ' '), 1);
t.equal(Cast.compare('\n0', '\n-1'), 1);
t.equal(Cast.compare('', 'false'), -1);
t.equal(Cast.compare('', ' false'), -1);
t.equal(Cast.compare('\n', ' false'), -1);
t.equal(Cast.compare(false, ''), 1);
t.equal(Cast.compare(false, ' '), 1);
t.end();
});
+1 -1
View File
@@ -18,7 +18,7 @@ test('all runtimeFunctions can be used together', t => {
t.end();
});
test('comparisons', t => {
test('comparison functions are equivalent to Cast.compare', t => {
const VALUES = [
0,
-0,