Skip to content

Commit b8e9396

Browse files
authoredDec 6, 2023
perf(codec): improve bytify and hexify perf (#580)
1 parent 8424b07 commit b8e9396

File tree

5 files changed

+119
-46
lines changed

5 files changed

+119
-46
lines changed
 

‎.changeset/big-ears-allow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ckb-lumos/codec": patch
3+
---
4+
5+
improving `hexify` and `bytify` performance

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@typescript-eslint/eslint-plugin": "^5.59.2",
2424
"@typescript-eslint/parser": "^5.59.2",
2525
"ava": "^3.8.2",
26+
"benchmark": "^2.1.4",
2627
"c8": "^7.10.0",
2728
"eslint": "^8.40.0",
2829
"eslint-import-resolver-typescript": "^2.7.0",

‎packages/codec/src/bytes.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,39 @@ export function bytifyRawString(rawString: string): Uint8Array {
1414
return new Uint8Array(buffer);
1515
}
1616

17+
const CHAR_0 = "0".charCodeAt(0); // 48
18+
const CHAR_9 = "9".charCodeAt(0); // 57
19+
const CHAR_A = "A".charCodeAt(0); // 65
20+
const CHAR_F = "F".charCodeAt(0); // 70
21+
const CHAR_a = "a".charCodeAt(0); // 97
22+
// const CHAR_f = "f".charCodeAt(0); // 102
23+
1724
function bytifyHex(hex: string): Uint8Array {
1825
assertHexString(hex);
1926

20-
hex = hex.slice(2);
21-
const uint8s = [];
22-
for (let i = 0; i < hex.length; i += 2) {
23-
uint8s.push(parseInt(hex.substr(i, 2), 16));
27+
const u8a = Uint8Array.from({ length: hex.length / 2 - 1 });
28+
29+
for (let i = 2, j = 0; i < hex.length; i = i + 2, j++) {
30+
const c1 = hex.charCodeAt(i);
31+
const c2 = hex.charCodeAt(i + 1);
32+
33+
// prettier-ignore
34+
const n1 = c1 <= CHAR_9 ? c1 - CHAR_0 : c1 <= CHAR_F ? c1 - CHAR_A + 10 : c1 - CHAR_a + 10
35+
// prettier-ignore
36+
const n2 = c2 <= CHAR_9 ? c2 - CHAR_0 : c2 <= CHAR_F ? c2 - CHAR_A + 10 : c2 - CHAR_a + 10
37+
38+
u8a[j] = (n1 << 4) | n2;
2439
}
2540

26-
return Uint8Array.from(uint8s);
41+
return u8a;
2742
}
2843

2944
function bytifyArrayLike(xs: ArrayLike<number>): Uint8Array {
30-
const isValidU8Vec = Array.from(xs).every((v) => v >= 0 && v <= 255);
31-
if (!isValidU8Vec) {
32-
throw new Error("invalid ArrayLike, all elements must be 0-255");
45+
for (let i = 0; i < xs.length; i++) {
46+
const v = xs[i];
47+
if (v < 0 || v > 255 || !Number.isInteger(v)) {
48+
throw new Error("invalid ArrayLike, all elements must be 0-255");
49+
}
3350
}
3451

3552
return Uint8Array.from(xs);
@@ -61,6 +78,10 @@ function equalUint8Array(a: Uint8Array, b: Uint8Array): boolean {
6178
}
6279
return true;
6380
}
81+
82+
const HEX_CACHE = Array.from({ length: 256 }).map((_, i) =>
83+
i.toString(16).padStart(2, "0")
84+
);
6485
/**
6586
* convert a {@link BytesLike} to an even length hex string prefixed with "0x"
6687
* @param buf
@@ -69,9 +90,13 @@ function equalUint8Array(a: Uint8Array, b: Uint8Array): boolean {
6990
* hexify(Buffer.from([1, 2, 3])) // "0x010203"
7091
*/
7192
export function hexify(buf: BytesLike): string {
72-
const hex = Array.from(bytify(buf))
73-
.map((b) => b.toString(16).padStart(2, "0"))
74-
.join("");
93+
let hex = "";
94+
95+
const u8a = bytify(buf);
96+
for (let i = 0; i < u8a.length; i++) {
97+
hex += HEX_CACHE[u8a[i]];
98+
}
99+
75100
return "0x" + hex;
76101
}
77102

‎packages/codec/src/utils.ts

+62-34
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,76 @@ import {
44
isCodecExecuteError,
55
} from "./error";
66

7-
const HEX_DECIMAL_REGEX = /^0x([0-9a-fA-F])+$/;
8-
const HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP = new Map<number, RegExp>();
7+
const CHAR_0 = "0".charCodeAt(0); // 48
8+
const CHAR_9 = "9".charCodeAt(0); // 57
9+
const CHAR_A = "A".charCodeAt(0); // 65
10+
const CHAR_F = "F".charCodeAt(0); // 70
11+
const CHAR_a = "a".charCodeAt(0); // 97
12+
const CHAR_f = "f".charCodeAt(0); // 102
913

10-
export function assertHexDecimal(str: string, byteLength?: number): void {
11-
if (byteLength) {
12-
let regex = HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP.get(byteLength);
13-
if (!regex) {
14-
const newRegex = new RegExp(`^0x([0-9a-fA-F]){1,${byteLength * 2}}$`);
15-
HEX_DECIMAL_WITH_BYTELENGTH_REGEX_MAP.set(byteLength, newRegex);
16-
regex = newRegex;
17-
}
18-
if (!regex.test(str)) {
19-
throw new Error("Invalid hex decimal!");
20-
}
21-
} else {
22-
if (!HEX_DECIMAL_REGEX.test(str)) {
23-
throw new Error("Invalid hex decimal!");
14+
function assertStartsWith0x(str: string): void {
15+
if (!str || !str.startsWith("0x")) {
16+
throw new Error("Invalid hex string");
17+
}
18+
}
19+
20+
function assertHexChars(str: string): void {
21+
const strLen = str.length;
22+
23+
for (let i = 2; i < strLen; i++) {
24+
const char = str[i].charCodeAt(0);
25+
if (
26+
(char >= CHAR_0 && char <= CHAR_9) ||
27+
(char >= CHAR_a && char <= CHAR_f) ||
28+
(char >= CHAR_A && char <= CHAR_F)
29+
) {
30+
continue;
2431
}
32+
33+
throw new Error(`Invalid hex character ${str[i]} in the string ${str}`);
2534
}
2635
}
2736

28-
const HEX_STRING_REGEX = /^0x([0-9a-fA-F][0-9a-fA-F])*$/;
29-
const HEX_STRING_WITH_BYTELENGTH_REGEX_MAP = new Map<number, RegExp>();
37+
export function assertHexDecimal(str: string, byteLength?: number): void {
38+
assertStartsWith0x(str);
39+
if (str.length === 2) {
40+
throw new Error(
41+
"Invalid hex decimal length, should be at least 1 character, the '0x' is incorrect, should be '0x0'"
42+
);
43+
}
44+
45+
const strLen = str.length;
3046

47+
if (typeof byteLength === "number" && strLen > byteLength * 2 + 2) {
48+
throw new Error(
49+
`Invalid hex decimal length, should be less than ${byteLength} bytes, got ${
50+
strLen / 2 - 1
51+
} bytes`
52+
);
53+
}
54+
55+
assertHexChars(str);
56+
}
57+
58+
/**
59+
* Assert if a string is a valid hex string that is matched with /^0x([0-9a-fA-F][0-9a-fA-F])*$/
60+
* @param str
61+
* @param byteLength
62+
*/
3163
export function assertHexString(str: string, byteLength?: number): void {
32-
if (byteLength) {
33-
let regex = HEX_STRING_WITH_BYTELENGTH_REGEX_MAP.get(byteLength);
34-
if (!regex) {
35-
const newRegex = new RegExp(
36-
`^0x([0-9a-fA-F][0-9a-fA-F]){${byteLength}}$`
37-
);
38-
HEX_STRING_WITH_BYTELENGTH_REGEX_MAP.set(byteLength, newRegex);
39-
regex = newRegex;
40-
}
41-
if (!regex.test(str)) {
42-
throw new Error("Invalid hex string!");
43-
}
44-
} else {
45-
if (!HEX_STRING_REGEX.test(str)) {
46-
throw new Error("Invalid hex string!");
47-
}
64+
assertStartsWith0x(str);
65+
66+
const strLen = str.length;
67+
68+
if (strLen % 2) {
69+
throw new Error("Invalid hex string length, must be even!");
4870
}
71+
72+
if (typeof byteLength === "number" && strLen !== byteLength * 2 + 2) {
73+
throw new Error("Invalid hex string length, not match with byteLength!");
74+
}
75+
76+
assertHexChars(str);
4977
}
5078

5179
export function assertUtf8String(str: string): void {

‎pnpm-lock.yaml

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

2 commit comments

Comments
 (2)

github-actions[bot] commented on Dec 6, 2023

@github-actions[bot]
Contributor

🚀 New canary release: 0.0.0-canary-b8e9396-20231206155226

npm install @ckb-lumos/lumos@0.0.0-canary-b8e9396-20231206155226

vercel[bot] commented on Dec 6, 2023

@vercel[bot]
Please sign in to comment.