Unroll u8aToBigInt loops (#1821)

* Unroll u8aToBigInt loops

* Bump dev

* Rolling tests
This commit is contained in:
Jaco
2023-05-13 10:51:46 +03:00
committed by GitHub
parent b73b96e412
commit e5373c35c8
7 changed files with 269 additions and 114 deletions
+1
View File
@@ -5,6 +5,7 @@
Changes:
- Align `u8aToBigInt` tests with `u8aToBn`
- Unroll loops on most-frequently used paths for `u8aToBigInt`
## 12.1.2 May 1, 2023
+60 -3
View File
@@ -54,6 +54,15 @@ describe('u8aToBigInt', (): void => {
).toBe('78563412');
});
it('converts values (i32)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0xf2, 0x34, 0x56, 0x78]),
{ isLe: false, isNegative: true }
).toString(16)
).toBe('-dcba988');
});
it('converts values (u40)', (): void => {
expect(
u8aToBigInt(
@@ -71,6 +80,29 @@ describe('u8aToBigInt', (): void => {
).toString(16)
).toBe('bc9a78563412');
});
it('converts values (u128)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412785634127856341278563412');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBigInt(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: true }
).toString(16)
).toBe(tstr.slice(0, i).reverse().join(''));
});
}
});
describe('signed', (): void => {
@@ -193,6 +225,29 @@ describe('u8aToBigInt', (): void => {
).toString(16)
).toBe('123456789abc');
});
it('converts values (u128)', (): void => {
expect(
u8aToBigInt(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678123456781234567812345678');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBigInt(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: false }
).toString(16)
).toBe(tstr.slice(0, i).join(''));
});
}
});
describe('empty creation', (): void => {
@@ -262,9 +317,11 @@ describe('u8aToBigInt', (): void => {
).toBe(256n);
});
perf('u8aToBigInt (u32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (i32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true }));
perf('u8aToBigInt (u64)', 500_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBigInt(v, { isNegative: true }));
perf('u8aToBigInt (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
perf('u8aToBigInt (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBigInt);
// perf('BigInt (constructor)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString());
// perf('BigInt (constructor -> string)', 1_000_000, [[12345678]], (v: number) => BigInt(v).toString());
+65 -20
View File
@@ -9,27 +9,43 @@ import { _1n } from '../bi/consts.js';
const U8_MAX = BigInt(256);
const U16_MAX = BigInt(256 * 256);
const U64_MAX = BigInt('0x10000000000000000');
/**
* @name u8aToBigInt
* @summary Creates a BigInt from a Uint8Array object.
*/
export function u8aToBigInt (value: Uint8Array, { isLe = true, isNegative = false }: ToBnOptions = {}): bigint {
if (!value || !value.length) {
return BigInt(0);
}
// BE is not the optimal path - LE is the SCALE default
const u8a = isLe
? value
: value.reverse();
const dvI = new DataView(u8a.buffer, u8a.byteOffset);
: value.slice().reverse();
const count = u8a.length;
const mod = count % 2;
let result = BigInt(0);
// This is mostly written for readability (with the single isNegative shortcut),
// as opposed to performance, e.g. `u8aToBn` does loop unrolling, etc.
if (isNegative) {
switch (count) {
case 0:
return BigInt(0);
case 1:
return BigInt(((u8a[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
return BigInt((((u8a[0] + (u8a[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
case 4:
return BigInt((((u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
}
const dvI = new DataView(u8a.buffer, u8a.byteOffset);
if (count === 8) {
return dvI.getBigInt64(0, true);
}
let result = BigInt(0);
const mod = count % 2;
for (let i = count - 2; i >= mod; i -= 2) {
result = (result * U16_MAX) + BigInt(dvI.getUint16(i, true) ^ 0xffff);
}
@@ -37,17 +53,46 @@ export function u8aToBigInt (value: Uint8Array, { isLe = true, isNegative = fals
if (mod) {
result = (result * U8_MAX) + BigInt(u8a[0] ^ 0xff);
}
} else {
for (let i = count - 2; i >= mod; i -= 2) {
result = (result * U16_MAX) + BigInt(dvI.getUint16(i, true));
}
if (mod) {
result = (result * U8_MAX) + BigInt(u8a[0]);
}
return (result * -_1n) - _1n;
}
return isNegative
? ((result * -_1n) - _1n)
: result;
switch (count) {
case 0:
return BigInt(0);
case 1:
return BigInt(u8a[0]);
case 2:
return BigInt(u8a[0] + (u8a[1] << 8));
case 4:
return BigInt(u8a[0] + (u8a[1] << 8) + (u8a[2] << 16) + (u8a[3] * 0x1_00_00_00));
}
const dvI = new DataView(u8a.buffer, u8a.byteOffset);
switch (count) {
case 8:
return dvI.getBigUint64(0, true);
case 16:
return (dvI.getBigUint64(8, true) * U64_MAX) + dvI.getBigUint64(0, true);
default: {
let result = BigInt(0);
const mod = count % 2;
for (let i = count - 2; i >= mod; i -= 2) {
result = (result * U16_MAX) + BigInt(dvI.getUint16(i, true));
}
if (mod) {
result = (result * U8_MAX) + BigInt(u8a[0]);
}
return result;
}
}
}
+60 -3
View File
@@ -72,6 +72,29 @@ describe('u8aToBn', (): void => {
).toString(16)
).toBe('bc9a78563412');
});
it('converts values (u128)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: true }
).toString(16)
).toBe('78563412785634127856341278563412');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBn(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: true }
).toString(16)
).toBe(tstr.slice(0, i).reverse().join(''));
});
}
});
describe('signed', (): void => {
@@ -177,6 +200,15 @@ describe('u8aToBn', (): void => {
).toBe('12345678');
});
it('converts values (i32)', (): void => {
expect(
u8aToBn(
new Uint8Array([0xf2, 0x34, 0x56, 0x78]),
{ isLe: false, isNegative: true }
).toString(16)
).toBe('-dcba988');
});
it('converts values (u40)', (): void => {
expect(
u8aToBn(
@@ -194,6 +226,29 @@ describe('u8aToBn', (): void => {
).toString(16)
).toBe('123456789abc');
});
it('converts values (u128)', (): void => {
expect(
u8aToBn(
new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78]),
{ isLe: false }
).toString(16)
).toBe('12345678123456781234567812345678');
});
for (let i = 1; i < 32; i++) {
const tu8a = [0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78];
const tstr = tu8a.map((n) => n.toString(16));
it(`converts values with length ${i}`, (): void => {
expect(
u8aToBn(
new Uint8Array(tu8a.slice(0, i)),
{ isLe: false }
).toString(16)
).toBe(tstr.slice(0, i).join(''));
});
}
});
describe('empty creation', (): void => {
@@ -263,9 +318,11 @@ describe('u8aToBn', (): void => {
).toBe(256);
});
perf('u8aToBn (u32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (i32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true }));
perf('u8aToBn (u64)', 500_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToBn(v, { isNegative: true }));
perf('u8aToBn (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (u64)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
perf('u8aToBn (u128)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c, 0x68, 0x65, 0x6c, 0x6c])]], u8aToBn);
// perf('BN (constructor)', 1_000_000, [[12345678]], (v: number) => new BN(v));
// perf('BN (constructor -> string)', 1_000_000, [[12345678]], (v: number) => new BN(v).toString());
+74 -69
View File
@@ -28,95 +28,100 @@ export function u8aToBn (value: Uint8Array, { isLe = true, isNegative = false }:
// shortcut for <= u48 values - in this case the manual conversion
// here seems to be more efficient than passing the full array
if (count <= 6) {
if (isNegative) {
let result = 0;
if (isLe) {
// Most common case i{8, 16, 32} default LE SCALE-encoded
// For <= 32, we also optimize the xor to a single op
// (see the comments around unrolling in the next section)
switch (count) {
case 0:
return new BN(0);
case 1:
result = value[0] ^ 0x0000_00ff;
break;
case 2:
result = (value[0] + (value[1] << 8)) ^ 0x0000_ffff;
break;
case 3:
result = (value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff;
break;
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
result = (value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff;
break;
case 5:
result = ((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00);
break;
default: // 6
result = ((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);
break;
}
} else {
for (let i = 0; i < count; i++) {
result = (result * 0x1_00) + (value[i] ^ 0xff);
}
}
return count
? new BN((result * -1) - 1)
: new BN(0);
} else 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) {
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]);
return new BN(((value[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
return new BN(value[0] + (value[1] << 8));
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));
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));
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] + (value[4] << 8)) * 0x1_00_00_00));
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);
default: // 6
return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8) + (value[5] << 16)) * 0x1_00_00_00));
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);
}
} else {
let result = 0;
}
for (let i = 0; i < count; i++) {
result = (result * 0x1_00) + value[i];
}
if (count === 0) {
return new BN(0);
} else if (count > 6) {
return new BN(value, 'be').fromTwos(value.length * 8);
}
return new BN(result);
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
switch (count) {
case 0:
return new BN(0);
case 1:
return new BN(value[0]);
case 2:
return new BN(value[0] + (value[1] << 8));
case 3:
return new BN(value[0] + (value[1] << 8) + (value[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(value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00));
case 5:
return new BN(value[0] + (value[1] << 8) + (value[2] << 16) + ((value[3] + (value[4] << 8)) * 0x1_00_00_00));
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));
default:
return new BN(value, 'le');
}
}
return isNegative
? new BN(value, isLe ? 'le' : 'be').fromTwos(value.length * 8)
: new BN(value, isLe ? 'le' : 'be');
if (count === 0) {
return new BN(0);
} else if (count > 6) {
return new BN(value, 'be');
}
let result = 0;
for (let i = 0; i < count; i++) {
result = (result * 0x1_00) + value[i];
}
return new BN(result);
}
+2 -2
View File
@@ -132,6 +132,6 @@ describe('u8aToNumber', (): void => {
});
});
perf('u8aToNumber (u32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToNumber);
perf('u8aToNumber (i32)', 1_000_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true }));
perf('u8aToNumber (i32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], (v: Uint8Array) => u8aToNumber(v, { isNegative: true }));
perf('u8aToNumber (u32)', 750_000, [[new Uint8Array([0x68, 0x65, 0x6c, 0x6c])]], u8aToNumber);
});
+7 -17
View File
@@ -10,49 +10,39 @@ interface ToNumberOptions {
/**
* @name u8aToNumber
* @summary Creates a number from a Uint8Array object.
* @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;
if (isNegative) {
let result = 0;
switch (count) {
case 0:
return 0;
case 1:
result = value[0] ^ 0x0000_00ff;
break;
return (((value[0] ^ 0x0000_00ff) * -1) - 1);
case 2:
result = (value[0] + (value[1] << 8)) ^ 0x0000_ffff;
break;
return ((((value[0] + (value[1] << 8)) ^ 0x0000_ffff) * -1) - 1);
case 3:
result = (value[0] + (value[1] << 8) + (value[2] << 16)) ^ 0x00ff_ffff;
break;
return ((((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
result = (value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff;
break;
return ((((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) * -1) - 1);
case 5:
result = ((value[0] + (value[1] << 8) + (value[2] << 16) + (value[3] * 0x1_00_00_00)) ^ 0xffff_ffff) + ((value[4] ^ 0xff) * 0x1_00_00_00_00);
break;
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);
case 6:
result = ((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);
break;
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);
default:
throw new Error('Value more than 48-bits cannot be reliably converted');
}
return (result * -1) - 1;
}
switch (count) {