diff --git a/CHANGELOG.md b/CHANGELOG.md index 4170b196c..b1a868bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ Changes: - Add strongly typed `object{Entries, Keys, Values}` utilities - Expand tests for uncovered functions - Sync with upstream Substrate ss58 registry -- Optimizations for hex <-> u8a functions -- Allow `u8aEq` to use `Uint{8, 16, 32}Array` with correct alignment +- Optimize for `hexToU8a` and `u8aToHex` functions +- Optimize `u8aEq` to use `DataView` for compares ## 7.8.2 Nov 7, 2021 diff --git a/packages/util/src/hex/alphabet.ts b/packages/util/src/hex/alphabet.ts index 7fa9bb13e..9b45a6bce 100644 --- a/packages/util/src/hex/alphabet.ts +++ b/packages/util/src/hex/alphabet.ts @@ -17,7 +17,7 @@ for (let n = 0; n < 256; n++) { for (let i = 0; i < 256; i++) { for (let j = 0; j < 256; j++) { const hex = U8_TO_HEX[i] + U8_TO_HEX[j]; - const n = (j * 256) + i; + const n = (i * 256) + j; U16_TO_HEX[n] = hex; HEX_TO_U16[hex] = n; diff --git a/packages/util/src/hex/toU8a.spec.ts b/packages/util/src/hex/toU8a.spec.ts index 03a0b8b3c..b04a70a10 100644 --- a/packages/util/src/hex/toU8a.spec.ts +++ b/packages/util/src/hex/toU8a.spec.ts @@ -35,6 +35,12 @@ describe('hexToU8a', (): void => { ); }); + it('converts known bytes to their correct values', (): void => { + expect( + hexToU8a('0x68656c6c6f20776f726c64') // hello world (11 bytes, non-aligned) + ).toEqual(new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64])); + }); + it('fails when non-hex value provided', (): void => { expect( () => hexToU8a('notahex') diff --git a/packages/util/src/hex/toU8a.ts b/packages/util/src/hex/toU8a.ts index 5ee3305ab..576717cce 100644 --- a/packages/util/src/hex/toU8a.ts +++ b/packages/util/src/hex/toU8a.ts @@ -28,37 +28,25 @@ export function hexToU8a (_value?: HexString | string | null, bitLength = -1): U const value = hexStripPrefix(_value).toLowerCase(); const valLength = value.length / 2; - const resultLength = Math.ceil( + const endLength = Math.ceil( bitLength === -1 ? valLength : bitLength / 8 ); - const result = new Uint8Array(resultLength); - const offset = resultLength > valLength - ? resultLength - valLength + const result = new Uint8Array(endLength); + const offset = endLength > valLength + ? endLength - valLength : 0; + const dv = new DataView(result.buffer, offset); + const mod = (endLength - offset) % 2; + const length = endLength - offset - mod; - // With offsets, it becomes slightly trickier since we need to ensure the U16 - // is at the correct alignment as well. Due to this, we just focus on the simple - // case where we do no bitlength conversions (which is the most-taken path) - if (offset) { - for (let i = 0; i < resultLength; i++) { - result[i + offset] = HEX_TO_U8[value.substr(i * 2, 2)]; - } - } else { - // NOTE: It is only _slightly_ more optimal to use HEX_TO_U16 here as - // well. The overhead does not seem to be in the lookup map iteration, - // even with 640k inputs - const mod = resultLength % 2; - const u16 = new Uint16Array(result.buffer, 0, (resultLength - mod) / 2); + for (let i = 0; i < length; i += 2) { + dv.setUint16(i, HEX_TO_U16[value.substr(i * 2, 4)]); + } - for (let i = 0; i < u16.length; i++) { - u16[i] = HEX_TO_U16[value.substr(i * 4, 4)]; - } - - if (mod) { - result[resultLength - 1] = HEX_TO_U8[value.substr(value.length - 2, 2)]; - } + if (mod) { + dv.setUint8(length, HEX_TO_U8[value.substr(value.length - 2, 2)]); } return result; diff --git a/packages/util/src/u8a/eq.spec.ts b/packages/util/src/u8a/eq.spec.ts index 2b24a085f..b56cbe45e 100644 --- a/packages/util/src/u8a/eq.spec.ts +++ b/packages/util/src/u8a/eq.spec.ts @@ -31,6 +31,42 @@ describe('u8aEq', (): void => { ).toEqual(true); }); + it('returns false when the contents do not match (with u32 start)', (): void => { + expect( + u8aEq( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3]), + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4]) + ) + ).toEqual(false); + }); + + it('returns true when the contents do match (with u32 start)', (): void => { + expect( + u8aEq( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3]), + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3]) + ) + ).toEqual(true); + }); + + it('returns false when the contents do not match (u32 only)', (): void => { + expect( + u8aEq( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 9]) + ) + ).toEqual(false); + }); + + it('returns true when the contents do match (u32 only)', (): void => { + expect( + u8aEq( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) + ) + ).toEqual(true); + }); + it('returns true with equalvalent hex & Uint8array', (): void => { expect( u8aEq( diff --git a/packages/util/src/u8a/eq.ts b/packages/util/src/u8a/eq.ts index fc0a4d49d..21e986f7a 100644 --- a/packages/util/src/u8a/eq.ts +++ b/packages/util/src/u8a/eq.ts @@ -5,41 +5,26 @@ import type { HexString } from '../types'; import { u8aToU8a } from './toU8a'; -interface Constructor { - BYTES_PER_ELEMENT: number; - - new(...args: unknown[]): T; -} - -// Creates a Uint{16, 32}Array, ensuring that the alignment is correct -function createArray (Clazz: Constructor, value: Uint8Array): T { - // The byteOffset needs to match the size of the data, i.e. for Uint32 it needs to be 4 - // NOTE: DataView doesn't have this limitation, but getters are slower - const align = Clazz.BYTES_PER_ELEMENT; - - return value.byteOffset % align - ? new Clazz(value.buffer.slice(value.byteOffset), 0, value.length / align) - : new Clazz(value.buffer, value.byteOffset, value.length / align); -} - -function equalsArray (a: T, b: T): boolean { - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; -} - function equals (a: Uint8Array, b: Uint8Array): boolean { if (a.length === b.length) { - // NOTE: We don't do BigUint64Array, at this point it is significantly slower - return a.length % Uint32Array.BYTES_PER_ELEMENT - ? a.length % Uint16Array.BYTES_PER_ELEMENT - ? equalsArray(a, b) - : equalsArray(createArray(Uint16Array, a), createArray(Uint16Array, b)) - : equalsArray(createArray(Uint32Array, a), createArray(Uint32Array, b)); + const dvA = new DataView(a.buffer, a.byteOffset); + const dvB = new DataView(b.buffer, b.byteOffset); + const mod = a.length % 4; + const length = a.length - mod; + + for (let i = 0; i < length; i += 4) { + if (dvA.getUint32(i) !== dvB.getUint32(i)) { + return false; + } + } + + for (let i = length; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; } return false; diff --git a/packages/util/src/u8a/toHex.ts b/packages/util/src/u8a/toHex.ts index ae5697952..ae0fbab06 100644 --- a/packages/util/src/u8a/toHex.ts +++ b/packages/util/src/u8a/toHex.ts @@ -5,24 +5,19 @@ import type { HexString } from '../types'; import { U8_TO_HEX, U16_TO_HEX } from '../hex/alphabet'; -const U16_SIZE = Uint16Array.BYTES_PER_ELEMENT; - /** @internal */ function hex (value: Uint8Array): string { - const mod = value.length % U16_SIZE; - - // We need alignment of the Uint16Array, see u8aEq for further details - const u16 = value.byteOffset % U16_SIZE - ? new Uint16Array(value.buffer.slice(value.byteOffset), 0, (value.length - mod) / U16_SIZE) - : new Uint16Array(value.buffer, value.byteOffset, (value.length - mod) / U16_SIZE); + const mod = value.length % 2; + const length = value.length - mod; + const dv = new DataView(value.buffer, value.byteOffset); let result = ''; - for (let i = 0; i < u16.length; i++) { - result += U16_TO_HEX[u16[i]]; + for (let i = 0; i < length; i += 2) { + result += U16_TO_HEX[dv.getUint16(i)]; } if (mod) { - result += U8_TO_HEX[value[value.length - 1]]; + result += U8_TO_HEX[dv.getUint8(length)]; } return result;