Skip to content

Commit e52fe5f

Browse files
committed
fixup! lib: refactor internal webidl converters
Local benchmark before this pass was roughly: fractional: ~60-90M/sec wrap 8/16/32: ~62-74M/sec wrap long long: ~21M/sec enforceRange: ~132-195M/sec Final local run: fractional: ~221-298M/sec wrap 8/16/32: ~92-94M/sec wrap long long: ~27M/sec enforceRange: ~206-250M/sec Signed-off-by: Filip Skokan <[email protected]>
1 parent a99ddeb commit e52fe5f

2 files changed

Lines changed: 177 additions & 16 deletions

File tree

lib/internal/webidl.js

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const {
3535
isTypedArray,
3636
} = require('internal/util/types');
3737

38+
const BIGINT_2_63 = 1n << 63n;
39+
const BIGINT_2_64 = 1n << 64n;
40+
3841
const converters = { __proto__: null };
3942

4043
/**
@@ -309,9 +312,30 @@ function convertToInt(
309312
// representable in JavaScript's Number type as unambiguous integers.
310313
upperBound = NumberMAX_SAFE_INTEGER;
311314
lowerBound = signed ? NumberMIN_SAFE_INTEGER : 0;
312-
} else if (signedness === 'unsigned') {
315+
} else if (!signed) {
316+
// Spell out the common Web IDL integer sizes so hot converters avoid
317+
// recomputing powers of two on every call.
313318
lowerBound = 0;
314-
upperBound = pow2(bitLength) - 1;
319+
if (bitLength === 8) {
320+
upperBound = 0xff;
321+
} else if (bitLength === 16) {
322+
upperBound = 0xffff;
323+
} else if (bitLength === 32) {
324+
upperBound = 0xffff_ffff;
325+
} else {
326+
upperBound = pow2(bitLength) - 1;
327+
}
328+
} else if (bitLength === 8) {
329+
// Signed 8/16/32-bit conversions are mostly exercised through direct
330+
// convertToInt() calls, but keep their common bounds cheap too.
331+
lowerBound = -0x80;
332+
upperBound = 0x7f;
333+
} else if (bitLength === 16) {
334+
lowerBound = -0x8000;
335+
upperBound = 0x7fff;
336+
} else if (bitLength === 32) {
337+
lowerBound = -0x8000_0000;
338+
upperBound = 0x7fff_ffff;
315339
} else {
316340
lowerBound = -pow2(bitLength - 1);
317341
upperBound = pow2(bitLength - 1) - 1;
@@ -322,15 +346,54 @@ function convertToInt(
322346
// ConvertToInt path, except that -0 must become +0. This skips the
323347
// generic ToNumber and option handling without skipping observable
324348
// object coercion.
325-
if (typeof V === 'number' &&
326-
V >= lowerBound &&
327-
V <= upperBound &&
328-
MathTrunc(V) === V) {
329-
return V === 0 ? 0 : V;
330-
}
349+
let x;
350+
if (typeof V === 'number') {
351+
// For primitive Numbers, in-range non-[Clamp] conversion is either
352+
// identity or IntegerPart(V). This keeps the default and [EnforceRange]
353+
// paths out of the generic ToNumber/options flow.
354+
if (V >= lowerBound && V <= upperBound) {
355+
const integer = MathTrunc(V);
356+
if (integer === V) {
357+
return V === 0 ? 0 : V;
358+
}
359+
if (options === kEmptyObject || options.enforceRange || !options.clamp) {
360+
return integer === 0 ? 0 : integer;
361+
}
362+
return evenRound(V);
363+
}
364+
if (options !== kEmptyObject && options.enforceRange) {
365+
// Keep [EnforceRange] ahead of [Clamp] without falling through to
366+
// the shared check, which would observe options.enforceRange again.
367+
if (!NumberIsFinite(V)) {
368+
throw makeException(
369+
'is not a finite number.',
370+
options);
371+
}
372+
373+
const integer = integerPart(V);
374+
if (integer < lowerBound || integer > upperBound) {
375+
throw makeException(
376+
`is outside the expected range of ${lowerBound} to ${upperBound}.`,
377+
{ __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' });
378+
}
331379

332-
// Step 4: convert V with ECMA-262 ToNumber.
333-
let x = toNumber(V, options);
380+
return integer;
381+
}
382+
if (options !== kEmptyObject && options.clamp && !NumberIsNaN(V)) {
383+
// Out-of-range [Clamp] returns one of the already-computed bounds.
384+
if (V <= lowerBound) {
385+
return lowerBound === 0 ? 0 : lowerBound;
386+
}
387+
if (V >= upperBound) {
388+
return upperBound === 0 ? 0 : upperBound;
389+
}
390+
return evenRound(V);
391+
}
392+
x = V;
393+
} else {
394+
// Step 4: convert V with ECMA-262 ToNumber.
395+
x = toNumber(V, options);
396+
}
334397
// Step 5: normalize -0 to +0.
335398
if (x === 0) {
336399
x = 0;
@@ -387,30 +450,41 @@ function convertToInt(
387450
// BigInt keeps x modulo 2^64 and the signed high-bit adjustment exact
388451
// before this helper returns the JavaScript binding result.
389452
let xBigInt = BigInt(x);
390-
const twoToTheBitLength = 1n << 64n;
391-
xBigInt = bigIntModulo(xBigInt, twoToTheBitLength);
453+
xBigInt = bigIntModulo(xBigInt, BIGINT_2_64);
392454

393455
// For long long and unsigned long long values outside the safe-integer
394456
// range, Web IDL says the JS Number value represents the closest numeric
395457
// value, choosing the value with an even significand if there are two
396458
// equally close values. Number(BigInt) performs that final approximation.
397459

398460
// Step 11: wrap into the signed range when the high bit is set.
399-
if (signed && xBigInt >= (1n << 63n)) {
400-
return Number(xBigInt - twoToTheBitLength);
461+
if (signed && xBigInt >= BIGINT_2_63) {
462+
return Number(xBigInt - BIGINT_2_64);
401463
}
402464

403465
// Step 12: return the unsigned value.
404466
return Number(xBigInt);
405467
}
406468

469+
// For 8/16/32-bit conversions, bitwise operators perform the same
470+
// power-of-two wrapping as Web IDL step 10 for finite integer Numbers.
471+
// The shifts narrow the unsigned value into the signed range when needed.
472+
if (bitLength === 8) {
473+
return signed ? (x << 24) >> 24 : x & 0xff;
474+
}
475+
if (bitLength === 16) {
476+
return signed ? (x << 16) >> 16 : x & 0xffff;
477+
}
478+
if (bitLength === 32) {
479+
return signed ? x | 0 : x >>> 0;
480+
}
481+
407482
// Step 10: reduce modulo 2^bitLength.
408483
const twoToTheBitLength = pow2(bitLength);
409-
const twoToOneLessThanTheBitLength = pow2(bitLength - 1);
410484
x = modulo(x, twoToTheBitLength);
411485

412486
// Step 11: wrap into the signed range when the high bit is set.
413-
if (signed && x >= twoToOneLessThanTheBitLength) {
487+
if (signed && x >= pow2(bitLength - 1)) {
414488
return x - twoToTheBitLength;
415489
}
416490

test/parallel/test-internal-webidl-converttoint.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ assert.strictEqual(convertToInt(-0.5, 64), 0);
3131
assert.strictEqual(convertToInt(-0.5, 64, 'signed'), 0);
3232
assert.strictEqual(convertToInt(-1.5, 64, 'signed'), -1);
3333

34+
{
35+
const options = {
36+
get enforceRange() {
37+
throw new Error('enforceRange should not be read');
38+
},
39+
get clamp() {
40+
throw new Error('clamp should not be read');
41+
},
42+
};
43+
44+
assert.strictEqual(convertToInt(7, 8, 'unsigned', options), 7);
45+
}
46+
3447
{
3548
const opts = {
3649
__proto__: null,
@@ -215,6 +228,12 @@ assert.strictEqual(convertToInt(Number.MAX_SAFE_INTEGER, 64, 'signed', {
215228
assert.strictEqual(convertToInt(Number.MIN_SAFE_INTEGER, 64, 'signed', {
216229
enforceRange: true,
217230
}), Number.MIN_SAFE_INTEGER);
231+
assert.strictEqual(convertToInt(-0.5, 8, 'unsigned', {
232+
enforceRange: true,
233+
}), 0);
234+
assert.strictEqual(convertToInt(255.5, 8, 'unsigned', {
235+
enforceRange: true,
236+
}), 255);
218237

219238
const outOfRangeValues = [2 ** 53, -(2 ** 53)];
220239
for (const value of outOfRangeValues) {
@@ -237,14 +256,74 @@ assert.throws(() => convertToInt(Number.MIN_SAFE_INTEGER - 1, 64, 'signed', {
237256
name: 'TypeError',
238257
code: 'ERR_OUT_OF_RANGE',
239258
});
259+
assert.throws(() => convertToInt(256, 8, 'unsigned', {
260+
enforceRange: true,
261+
}), {
262+
name: 'TypeError',
263+
code: 'ERR_OUT_OF_RANGE',
264+
});
265+
266+
{
267+
const calls = [];
268+
const options = {
269+
get enforceRange() {
270+
calls.push('enforceRange');
271+
return true;
272+
},
273+
get clamp() {
274+
calls.push('clamp');
275+
return true;
276+
},
277+
};
278+
279+
assert.strictEqual(convertToInt(1.5, 8, 'unsigned', options), 1);
280+
assert.deepStrictEqual(calls, ['enforceRange']);
281+
}
282+
283+
{
284+
const calls = [];
285+
const options = {
286+
get enforceRange() {
287+
calls.push('enforceRange');
288+
return true;
289+
},
290+
get clamp() {
291+
calls.push('clamp');
292+
return true;
293+
},
294+
};
295+
296+
assert.strictEqual(convertToInt(255.5, 8, 'unsigned', options), 255);
297+
assert.deepStrictEqual(calls, ['enforceRange']);
298+
}
299+
300+
{
301+
const calls = [];
302+
const options = {
303+
get enforceRange() {
304+
calls.push('enforceRange');
305+
return false;
306+
},
307+
get clamp() {
308+
calls.push('clamp');
309+
return true;
310+
},
311+
};
312+
313+
assert.strictEqual(convertToInt(256, 8, 'unsigned', options), 255);
314+
assert.deepStrictEqual(calls, ['enforceRange', 'clamp']);
315+
}
240316

241317
// Out of range: clamp
242318
assert.strictEqual(convertToInt(NaN, 64, 'unsigned', { clamp: true }), 0);
243319
assert.strictEqual(convertToInt(Infinity, 64, 'unsigned', { clamp: true }), Number.MAX_SAFE_INTEGER);
244320
assert.strictEqual(convertToInt(-Infinity, 64, 'unsigned', { clamp: true }), 0);
245321
assert.strictEqual(convertToInt(-Infinity, 64, 'signed', { clamp: true }), Number.MIN_SAFE_INTEGER);
322+
assert.strictEqual(convertToInt(0x1_0000, 16, 'unsigned', { clamp: true }), 0xFFFF);
246323
assert.strictEqual(convertToInt(0x1_0000_0000, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF);
247324
assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF);
325+
assert.strictEqual(convertToInt(0x8000, 16, 'signed', { clamp: true }), 0x7FFF);
326+
assert.strictEqual(convertToInt(-0x8001, 16, 'signed', { clamp: true }), -0x8000);
248327
assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed', { clamp: true }), 0x7FFF_FFFF);
249328
assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'signed', { clamp: true }), 0x7FFF_FFFF);
250329
assert.strictEqual(convertToInt(0.5, 64, 'unsigned', { clamp: true }), 0);
@@ -261,6 +340,10 @@ assert.strictEqual(convertToInt(Infinity, 64), 0);
261340
assert.strictEqual(convertToInt(-Infinity, 64), 0);
262341
assert.strictEqual(convertToInt(-3, 8), 253);
263342
assert.strictEqual(convertToInt(-3, 8), new Uint8Array([-3])[0]);
343+
assert.strictEqual(convertToInt(0x1_0000, 16), 0);
344+
assert.strictEqual(convertToInt(0x1_0001, 16), 1);
345+
assert.strictEqual(convertToInt(-1, 16), 0xFFFF);
346+
assert.strictEqual(convertToInt(-1, 16), new Uint16Array([-1])[0]);
264347
assert.strictEqual(convertToInt(0x1_0000_0000, 32), 0);
265348
assert.strictEqual(convertToInt(0x1_0000_0001, 32), 1);
266349
assert.strictEqual(convertToInt(0xFFFF_FFFF, 32), 0xFFFF_FFFF);
@@ -274,6 +357,10 @@ assert.strictEqual(convertToInt(-(two64 + 2 ** 12), 64),
274357
two64 - 2 ** 12);
275358

276359
// Out of range, step 11.
360+
assert.strictEqual(convertToInt(0x8000, 16, 'signed'), -0x8000);
361+
assert.strictEqual(convertToInt(0xFFFF, 16, 'signed'), -1);
362+
assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), 0x7FFF);
363+
assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), new Int16Array([-0x8001])[0]);
277364
assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed'), -0x8000_0000);
278365
assert.strictEqual(convertToInt(0xFFF_FFFF, 32, 'signed'), 0xFFF_FFFF);
279366
assert.strictEqual(convertToInt(-200, 8, 'signed'), 56);

0 commit comments

Comments
 (0)