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:
1 parent
cb2eba9eb3
commit
4aa167e523
4 files changed
+68
-14
No files matched your search
+20
-11
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user