Skip to content

Commit

Permalink
Remove explicit BE (swap to non-optimal reverse)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacogr committed Aug 22, 2023
1 parent 31c1a9a commit a847d66
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 105 deletions.
5 changes: 3 additions & 2 deletions packages/util/src/u8a/toBigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
120 changes: 45 additions & 75 deletions packages/util/src/u8a/toBn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
11 changes: 4 additions & 7 deletions packages/util/src/u8a/toNumber.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
42 changes: 21 additions & 21 deletions packages/util/src/u8a/toNumber.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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');
Expand Down

0 comments on commit a847d66

Please sign in to comment.