mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-09 15:48:08 -05:00
feat: fromHexInto() api (#8275)
**Motivation**
- improve the time to deserialize hex, especially for getBlobsV2() where
each blob is 131kb
- if the benchmark is correct, we can expect the time to deserialize
blob hex from `332.6842 ms/op` to more or less 2ms
- will apply it for getBlobsV2() in the next PR by preallocate some
memory and reuse it for all slots
**Description**
- implement `fromHexInto()` using `String.charCodeAt()` for browser and
use that for NodeJs as well
- the Buffer/NodeJS implementation is too bad that I only maintain it in
the benchmark
**Test result on a regular lodestar node**
- `browser fromHexInto(blob)` is 1000x faster than `browser
fromHex(blob)` and >100x faster than `nodejs fromHexInto(blob) `
```
packages/utils/test/perf/bytes.test.ts
bytes utils
✔ nodejs block root to RootHex using toHex 2817687 ops/s 354.9010 ns/op - 1324 runs 0.897 s
✔ nodejs block root to RootHex using toRootHex 4369044 ops/s 228.8830 ns/op - 1793 runs 0.902 s
✔ nodejs fromhex(blob) 3.005854 ops/s 332.6842 ms/op - 10 runs 4.18 s
✔ nodejs fromHexInto(blob) 3.617654 ops/s 276.4222 ms/op - 10 runs 3.36 s
✔ browser block root to RootHex using the deprecated toHexString 1656696 ops/s 603.6110 ns/op - 963 runs 1.34 s
✔ browser block root to RootHex using toHex 2060611 ops/s 485.2930 ns/op - 424 runs 0.812 s
✔ browser block root to RootHex using toRootHex 2320476 ops/s 430.9460 ns/op - 889 runs 0.841 s
✔ browser fromHexInto(blob) 503.7166 ops/s 1.985243 ms/op - 10 runs 21.9 s
✔ browser fromHex(blob) 0.5095370 ops/s 1.962566 s/op - 10 runs 21.7 s
```
---------
Co-authored-by: Tuyen Nguyen <twoeths@users.noreply.github.com>
This commit is contained in:
@@ -60,12 +60,29 @@ export function fromHex(hex: string): Uint8Array {
|
||||
const byteLen = hex.length / 2;
|
||||
const bytes = new Uint8Array(byteLen);
|
||||
for (let i = 0; i < byteLen; i++) {
|
||||
const byte = parseInt(hex.slice(i * 2, (i + 1) * 2), 16);
|
||||
bytes[i] = byte;
|
||||
const byte2i = charCodeToByte(hex.charCodeAt(i * 2));
|
||||
const byte2i1 = charCodeToByte(hex.charCodeAt(i * 2 + 1));
|
||||
bytes[i] = (byte2i << 4) | byte2i1;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function fromHexInto(hex: string, buffer: Uint8Array): void {
|
||||
if (hex.startsWith("0x")) {
|
||||
hex = hex.slice(2);
|
||||
}
|
||||
|
||||
if (hex.length !== buffer.length * 2) {
|
||||
throw new Error(`hex string length ${hex.length} must be exactly double the buffer length ${buffer.length}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const byte2i = charCodeToByte(hex.charCodeAt(i * 2));
|
||||
const byte2i1 = charCodeToByte(hex.charCodeAt(i * 2 + 1));
|
||||
buffer[i] = (byte2i << 4) | byte2i1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate charCodes from bytes. Note that charCodes index 0 and 1 ("0x") are not populated.
|
||||
*/
|
||||
@@ -85,3 +102,22 @@ function bytesIntoCharCodes(bytes: Uint8Array, charCodes: number[]): void {
|
||||
charCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87;
|
||||
}
|
||||
}
|
||||
|
||||
function charCodeToByte(charCode: number): number {
|
||||
// "a".charCodeAt(0) = 97, "f".charCodeAt(0) = 102 => delta = 87
|
||||
if (charCode >= 97 && charCode <= 102) {
|
||||
return charCode - 87;
|
||||
}
|
||||
|
||||
// "A".charCodeAt(0) = 65, "F".charCodeAt(0) = 70 => delta = 55
|
||||
if (charCode >= 65 && charCode <= 70) {
|
||||
return charCode - 55;
|
||||
}
|
||||
|
||||
// "0".charCodeAt(0) = 48, "9".charCodeAt(0) = 57 => delta = 48
|
||||
if (charCode >= 48 && charCode <= 57) {
|
||||
return charCode - 48;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid hex character code: ${charCode}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
fromHex as browserFromHex,
|
||||
fromHexInto as browserFromHexInto,
|
||||
toHex as browserToHex,
|
||||
toPubkeyHex as browserToPubkeyHex,
|
||||
toRootHex as browserToRootHex,
|
||||
@@ -15,6 +16,8 @@ let toHex = browserToHex;
|
||||
let toRootHex = browserToRootHex;
|
||||
let toPubkeyHex = browserToPubkeyHex;
|
||||
let fromHex = browserFromHex;
|
||||
// there is no fromHexInto for NodeJs as the performance of browserFromHexInto is >100x faster
|
||||
const fromHexInto = browserFromHexInto;
|
||||
|
||||
if (typeof Buffer !== "undefined") {
|
||||
toHex = nodeToHex;
|
||||
@@ -23,4 +26,4 @@ if (typeof Buffer !== "undefined") {
|
||||
fromHex = nodeFromHex;
|
||||
}
|
||||
|
||||
export {toHex, toRootHex, toPubkeyHex, fromHex};
|
||||
export {toHex, toRootHex, toPubkeyHex, fromHex, fromHexInto};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {fromHexInto as _fromHexInto} from "./browser.js";
|
||||
|
||||
export function toHex(buffer: Uint8Array | Parameters<typeof Buffer.from>[0]): string {
|
||||
if (Buffer.isBuffer(buffer)) {
|
||||
return "0x" + buffer.toString("hex");
|
||||
@@ -59,3 +61,5 @@ export function fromHex(hex: string): Uint8Array {
|
||||
const b = Buffer.from(hex, "hex");
|
||||
return new Uint8Array(b.buffer, b.byteOffset, b.length);
|
||||
}
|
||||
|
||||
/// the performance of fromHexInto using a preallocated buffer is very bad compared to browser so I moved it to the benchmark
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import {bench, describe} from "@chainsafe/benchmark";
|
||||
import {toHexString} from "../../src/bytes.js";
|
||||
import {toHex as browserToHex, toRootHex as browserToRootHex} from "../../src/bytes/browser.js";
|
||||
import {toHex, toRootHex} from "../../src/bytes/nodejs.js";
|
||||
import {
|
||||
fromHex as browserFromHex,
|
||||
fromHexInto as browserFromHexInto,
|
||||
toHex as browserToHex,
|
||||
toRootHex as browserToRootHex,
|
||||
} from "../../src/bytes/browser.js";
|
||||
import {fromHex, toHex, toRootHex} from "../../src/bytes/nodejs.js";
|
||||
|
||||
describe("bytes utils", () => {
|
||||
const runsFactor = 1000;
|
||||
const blockRoot = new Uint8Array(Array.from({length: 32}, (_, i) => i));
|
||||
// FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT = 4096 * 32 = 131072
|
||||
const BLOB_LEN = 131072;
|
||||
const blob = new Uint8Array(BLOB_LEN);
|
||||
for (let i = 0; i < blob.length; i++) {
|
||||
blob[i] = i % 256;
|
||||
}
|
||||
const blobHex = toHex(blob);
|
||||
|
||||
bench({
|
||||
id: "nodejs block root to RootHex using toHex",
|
||||
@@ -27,6 +39,25 @@ describe("bytes utils", () => {
|
||||
runsFactor,
|
||||
});
|
||||
|
||||
bench({
|
||||
id: "nodejs fromhex(blob)",
|
||||
fn: () => {
|
||||
for (let i = 0; i < runsFactor; i++) {
|
||||
fromHex(blobHex);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const buffer = Buffer.alloc(BLOB_LEN);
|
||||
bench({
|
||||
id: "nodejs fromHexInto(blob)",
|
||||
fn: () => {
|
||||
for (let i = 0; i < runsFactor; i++) {
|
||||
nodeJsFromHexInto(blobHex, buffer);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
bench({
|
||||
id: "browser block root to RootHex using the deprecated toHexString",
|
||||
fn: () => {
|
||||
@@ -56,4 +87,41 @@ describe("bytes utils", () => {
|
||||
},
|
||||
runsFactor,
|
||||
});
|
||||
|
||||
const buf = new Uint8Array(BLOB_LEN);
|
||||
bench({
|
||||
id: "browser fromHexInto(blob)",
|
||||
fn: () => {
|
||||
for (let i = 0; i < runsFactor; i++) {
|
||||
browserFromHexInto(blobHex, buf);
|
||||
}
|
||||
},
|
||||
runsFactor,
|
||||
});
|
||||
|
||||
bench({
|
||||
id: "browser fromHex(blob)",
|
||||
fn: () => {
|
||||
for (let i = 0; i < runsFactor; i++) {
|
||||
browserFromHex(blobHex);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* this function is so slow compared to browser's implementation we only maintain it here to compare performance
|
||||
* - nodejs fromHexInto(blob) 3.562495 ops/s 280.7022 ms/op - 10 runs 3.50 s
|
||||
* - browser fromHexInto(blob) 535.0952 ops/s 1.868826 ms/op - 10 runs 20.8 s
|
||||
*/
|
||||
function nodeJsFromHexInto(hex: string, buffer: Buffer): void {
|
||||
if (hex.startsWith("0x")) {
|
||||
hex = hex.slice(2);
|
||||
}
|
||||
|
||||
if (hex.length !== buffer.length * 2) {
|
||||
throw new Error(`hex string length ${hex.length} must be exactly double the buffer length ${buffer.length}`);
|
||||
}
|
||||
|
||||
buffer.write(hex, "hex");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
bytesToInt,
|
||||
formatBytes,
|
||||
fromHex,
|
||||
fromHexInto,
|
||||
intToBytes,
|
||||
toHex,
|
||||
toHexString,
|
||||
@@ -108,16 +109,26 @@ describe("toPubkeyHex", () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("fromHex", () => {
|
||||
describe("fromHex and fromHexInto", () => {
|
||||
const testCases: {input: string; output: Buffer | Uint8Array}[] = [
|
||||
{
|
||||
input: "0x48656c6c6f2c20576f726c6421",
|
||||
output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]),
|
||||
},
|
||||
{
|
||||
// same but with upper case
|
||||
input: "0x48656C6C6F2C20576F726C6421",
|
||||
output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]),
|
||||
},
|
||||
{
|
||||
input: "48656c6c6f2c20576f726c6421",
|
||||
output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]),
|
||||
},
|
||||
{
|
||||
// same but with upper case
|
||||
input: "48656C6C6F2C20576F726C6421",
|
||||
output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]),
|
||||
},
|
||||
{input: "0x", output: new Uint8Array([])},
|
||||
];
|
||||
|
||||
@@ -126,6 +137,14 @@ describe("fromHex", () => {
|
||||
expect(fromHex(input)).toEqual(output);
|
||||
});
|
||||
}
|
||||
|
||||
for (const {input, output} of testCases) {
|
||||
it(`should convert hex string ${input} into provided buffer`, () => {
|
||||
const buffer = new Uint8Array(output.length);
|
||||
fromHexInto(input, buffer);
|
||||
expect(toHex(buffer)).toBe(toHex(output));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("toHexString", () => {
|
||||
|
||||
Reference in New Issue
Block a user