From a847d66d8e040b2b7c5d6dc25b17f8a1a9488447 Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 22 Aug 2023 13:12:28 +0300 Subject: [PATCH] Remove explicit BE (swap to non-optimal reverse) --- packages/util/src/u8a/toBigInt.ts | 5 +- packages/util/src/u8a/toBn.ts | 120 ++++++++++--------------- packages/util/src/u8a/toNumber.spec.ts | 11 +-- packages/util/src/u8a/toNumber.ts | 42 ++++----- 4 files changed, 73 insertions(+), 105 deletions(-) diff --git a/packages/util/src/u8a/toBigInt.ts b/packages/util/src/u8a/toBigInt.ts index fbe2a7dd90..d468335510 100644 --- a/packages/util/src/u8a/toBigInt.ts +++ b/packages/util/src/u8a/toBigInt.ts @@ -16,13 +16,14 @@ const U64_MAX = BigInt('0x10000000000000000'); * @summary Creates a BigInt from a Uint8Array object. */ export function u8aToBigInt (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): bigint { - // BE is not the optimal path - LE is the SCALE default + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interseted in (the BE is added for the sake of being comprehensive) const u8a = isLe ? value : value.slice().reverse(); const count = u8a.length; - if (isNegative && (u8a[count - 1] & 0x80)) { + if (isNegative && (!count || (u8a[count - 1] & 0x80))) { switch (count) { case 0: return BigInt(0); diff --git a/packages/util/src/u8a/toBn.ts b/packages/util/src/u8a/toBn.ts index 9838e69a60..521108e42c 100644 --- a/packages/util/src/u8a/toBn.ts +++ b/packages/util/src/u8a/toBn.ts @@ -24,107 +24,77 @@ import { BN } from '../bn/bn.js'; * ``` */ export function u8aToBn (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): BN { - const count = value.length; - const signByte = isLe - ? value[count - 1] - : value[0]; + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interseted in (the BE is added for the sake of being comprehensive) + const u8a = isLe + ? value + : value.slice().reverse(); + const count = u8a.length; // shortcut for <= u48 values - in this case the manual conversion // here seems to be more efficient than passing the full array - if (isNegative && (signByte & 0x80)) { - if (isLe) { - // Most common case i{8, 16, 32} default LE SCALE-encoded - // For <= 32, we also optimize the xor to a single op - switch (count) { - case 0: - return new BN(0); - - case 1: - return new BN(((value[0] ^ 0x0000_00ff) * -1) - 1); - - case 2: - return new BN((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1); - - case 3: - return new BN((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); - - case 4: - // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to - // 32-bit, in the case where the top-most bit is set this yields a negative value - return new BN((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); - - case 5: - return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); - - case 6: - return new BN(((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); - - default: - return new BN(value, 'le').fromTwos(value.length * 8); - } - } - - if (count === 0) { - return new BN(0); - } else if (count > 6) { - return new BN(value, 'be').fromTwos(value.length * 8); - } - - let result = 0; - - for (let i = 0; i < count; i++) { - result = (result * 0x1_00) + (value[i] ^ 0xff); - } - - return new BN((result * -1) - 1); - } - - if (isLe) { - // Most common case - u{8, 16, 32} default LE SCALE-encoded - // - // There are some slight benefits in unrolling this specific loop, - // however it comes with diminishing returns since here the actual - // `new BN` does seem to take up the bulk of the time + if (isNegative && (!count || (u8a[count - 1] & 0x80))) { + // Most common case i{8, 16, 32} default LE SCALE-encoded + // For <= 32, we also optimize the xor to a single op switch (count) { case 0: return new BN(0); case 1: - return new BN(value[0]); + return new BN(((u8a[0] ^ 0x0000_00ff) * -1) - 1); case 2: - return new BN(value[0] + (value[1] << 8)); + return new BN((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1); case 3: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16)); + return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)); + return new BN((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); case 5: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00)); + return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); case 6: - return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00)); + return new BN(((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); default: - return new BN(value, 'le'); + return new BN(u8a, 'le').fromTwos(count * 8); } } - if (count === 0) { - return new BN(0); - } else if (count > 6) { - return new BN(value, 'be'); - } + // Most common case - u{8, 16, 32} default LE SCALE-encoded + // + // There are some slight benefits in unrolling this specific loop, + // however it comes with diminishing returns since here the actual + // `new BN` does seem to take up the bulk of the time + switch (count) { + case 0: + return new BN(0); - let result = 0; + case 1: + return new BN(u8a[0]); - for (let i = 0; i < count; i++) { - result = (result * 0x1_00) + value[i]; - } + case 2: + return new BN(u8a[0] + (u8a[1] << 8)); + + case 3: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)); + + case 4: + // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to + // 32-bit, in the case where the top-most bit is set this yields a negative value + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)); - return new BN(result); + case 5: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00)); + + case 6: + return new BN(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00)); + + default: + return new BN(u8a, 'le'); + } } diff --git a/packages/util/src/u8a/toNumber.spec.ts b/packages/util/src/u8a/toNumber.spec.ts index 3a14a43b7d..425f50c418 100644 --- a/packages/util/src/u8a/toNumber.spec.ts +++ b/packages/util/src/u8a/toNumber.spec.ts @@ -7,21 +7,18 @@ import { TESTS } from '../bn/toU8a.spec.js'; import { perf } from '../test/index.js'; import { u8aToNumber } from './index.js'; -const TESTS_NUM = TESTS.filter(([isLe,, numarr]) => - isLe === true && - numarr.length <= 6 -); +const TESTS_NUM = TESTS.filter(([,, numarr]) => numarr.length <= 6); describe('u8aToNumber', (): void => { describe('conversion tests', (): void => { for (let i = 0, count = TESTS_NUM.length; i < count; i++) { - const [, isNegative, numarr, strval] = TESTS_NUM[i]; + const [isLe, isNegative, numarr, strval] = TESTS_NUM[i]; - it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isNegative=${isNegative})`, (): void => { + it(`${i}: creates ${strval} (bitLength=${numarr.length * 8}, isLe=${isLe}, isNegative=${isNegative})`, (): void => { expect( u8aToNumber( new Uint8Array(numarr), - { isNegative } + { isLe, isNegative } ).toString() ).toBe(strval); }); diff --git a/packages/util/src/u8a/toNumber.ts b/packages/util/src/u8a/toNumber.ts index 7749f90d1a..cd96f23ad5 100644 --- a/packages/util/src/u8a/toNumber.ts +++ b/packages/util/src/u8a/toNumber.ts @@ -1,48 +1,48 @@ // Copyright 2017-2023 @polkadot/util authors & contributors // SPDX-License-Identifier: Apache-2.0 -interface ToNumberOptions { - /** - * @description Number is signed, apply two's complement - */ - isNegative?: boolean; -} +import type { ToBnOptions } from '../types.js'; /** * @name u8aToNumber * @summary Creates a number from a Uint8Array object. This only operates on LE values as used in SCALE. */ -export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumberOptions = {}): number { - const count = value.length; +export function u8aToNumber (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): number { + // slice + reverse is expensive, however SCALE is LE by default so this is the path + // we are most interseted in (the BE is added for the sake of being comprehensive) + const u8a = isLe + ? value + : value.slice().reverse(); + const count = u8a.length; // When the value is a i{8, 16, 24, 32, 40, 40} values and the top-most bit // indicates a signed value, we use a two's complement conversion. If one of these // flags are not set, we just do a normal unsigned conversion (the same shortcut // applies in both the u8aTo{BigInt, Bn} conversions as well) - if (isNegative && (value[count - 1] & 0x80)) { + if (isNegative && (!count || (u8a[count - 1] & 0x80))) { switch (count) { case 0: return 0; case 1: - return (((value[0] ^ 0x0000_00ff) * -1) - 1); + return (((u8a[0] ^ 0x0000_00ff) * -1) - 1); case 2: - return ((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1); case 3: - return ((((value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16)) ^ 0x00ff_ffff) * -1) - 1); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return ((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); + return ((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1); case 5: - return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); + return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((u8a[4] ^ 0xff) * 0x1_00_00_00_00)) * -1) - 1); case 6: - return (((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((value[4] + (value[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); + return (((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + (((u8a[4] + (u8a[5] << 8)) ^ 0x0000_ffff) * 0x1_00_00_00_00)) * -1) - 1); default: throw new Error('Value more than 48-bits cannot be reliably converted'); @@ -54,24 +54,24 @@ export function u8aToNumber (value: Uint8Array, { isNegative = false }: ToNumber return 0; case 1: - return value[0]; + return u8a[0]; case 2: - return value[0] + (value[1] << 8); + return u8a[0] + (u8a[1] << 8); case 3: - return value[0] + (value[1] << 8) + (value[2] << 16); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16); case 4: // for the 3rd byte, we don't << 24 - since JS converts all bitwise operators to // 32-bit, in the case where the top-most bit is set this yields a negative value - return value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00); case 5: - return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8)) * 0x1_00_00_00); case 6: - return value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00); + return u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + ((u8a[3] + (u8a[4] << 8) + (u8a[5] << 16)) * 0x1_00_00_00); default: throw new Error('Value more than 48-bits cannot be reliably converted');