From 6c2f861aad34f40902a2f867a0cdd6537374ccc6 Mon Sep 17 00:00:00 2001 From: mrfoxpro Date: Fri, 13 Sep 2024 21:51:48 +0500 Subject: [PATCH] cache DataView to improve performance --- serde-generate/runtime/typescript/serde.ts | 184 ++++++++++----------- suite/typescript/readme.md | 20 +-- 2 files changed, 95 insertions(+), 109 deletions(-) diff --git a/serde-generate/runtime/typescript/serde.ts b/serde-generate/runtime/typescript/serde.ts index 964cea3b1..fd67d7f29 100644 --- a/serde-generate/runtime/typescript/serde.ts +++ b/serde-generate/runtime/typescript/serde.ts @@ -26,7 +26,6 @@ export type WrapperOfCase = T extends { $: export interface Reader { readString(): string - readBytes(): Uint8Array readBool(): boolean readUnit(): null readChar(): string @@ -52,7 +51,6 @@ export interface Reader { export interface Writer { writeString(value: string): void - writeBytes(value: Uint8Array): void writeBool(value: boolean): void writeUnit(value: null): void writeChar(value: string): void @@ -76,58 +74,50 @@ export interface Writer { sortMapEntries(offsets: number[]): void } +const BIG_32 = 32n +const BIG_64 = 64n +const BIG_32Fs = 429967295n +const BIG_64Fs = 18446744073709551615n + export abstract class BinaryWriter implements Writer { - public static readonly BIG_32 = 32n - public static readonly BIG_64 = 64n - public static readonly BIG_32Fs = 429967295n - public static readonly BIG_64Fs = 18446744073709551615n public static readonly TEXT_ENCODER = new TextEncoder() - public buffer = new ArrayBuffer(8) + public view = new DataView(new ArrayBuffer(128)) public offset = 0 - private ensureBufferWillHandleSize(bytes: number) { - const wishSize = this.offset + bytes - if (wishSize > this.buffer.byteLength) { - let newBufferLength = this.buffer.byteLength - while (newBufferLength < wishSize) newBufferLength *= 2 + private alloc(allocLength: number) { + const wishSize = this.offset + allocLength + + const currentLength = this.view.buffer.byteLength + if (wishSize > currentLength) { + let newBufferLength = currentLength + while (newBufferLength <= wishSize) newBufferLength = newBufferLength << 1 // TODO: there is new API for resizing buffer, but in Node it seems to be slower then allocating new // this.buffer.resize(newBufferLength) const newBuffer = new Uint8Array(newBufferLength) - newBuffer.set(new Uint8Array(this.buffer)) - this.buffer = newBuffer.buffer + newBuffer.set(new Uint8Array(this.view.buffer)) + + this.view = new DataView(newBuffer.buffer) } } - protected write(values: Uint8Array) { - this.ensureBufferWillHandleSize(values.length) - new Uint8Array(this.buffer, this.offset).set(values) - this.offset += values.length - } - abstract writeLength(value: number): void abstract writeVariantIndex(value: number): void abstract sortMapEntries(offsets: number[]): void public writeString(value: string) { const bytes = value.length * 3 + 8 - this.ensureBufferWillHandleSize(bytes) + this.alloc(bytes) // TODO: check this for correctness - const { written } = BinaryWriter.TEXT_ENCODER.encodeInto(value, new Uint8Array(this.buffer, this.offset + 8)) + const { written } = BinaryWriter.TEXT_ENCODER.encodeInto(value, new Uint8Array(this.view.buffer, this.offset + 8)) this.writeU64(written) this.offset += written } - public writeBytes(value: Uint8Array) { - this.writeLength(value.length) - this.write(value) - } - public writeBool(value: boolean) { - const byteValue = value ? 1 : 0 - this.write(new Uint8Array([byteValue])) + this.writeU8(value ? 1 : 0) } // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/explicit-module-boundary-types @@ -135,28 +125,26 @@ export abstract class BinaryWriter implements Writer { return } - private writeWithFunction(fn: (byteOffset: number, value: number, littleEndian: boolean) => void, bytesLength: number, value: number) { - this.ensureBufferWillHandleSize(bytesLength) - const dv = new DataView(this.buffer, this.offset) - fn.apply(dv, [0, value, true]) - this.offset += bytesLength - } - public writeU8(value: number) { - this.write(new Uint8Array([value])) + this.alloc(1) + this.view.setUint8(this.offset, value) + this.offset += 1 } public writeU16(value: number) { - this.writeWithFunction(DataView.prototype.setUint16, 2, value) + this.alloc(2) + this.view.setUint16(this.offset, value, true) + this.offset += 2 } public writeU32(value: number) { - this.writeWithFunction(DataView.prototype.setUint32, 4, value) + this.alloc(4) + this.view.setUint32(this.offset, value, true) + this.offset += 4 } public writeU64(value: bigint | number) { - const low = BigInt(value) & BinaryWriter.BIG_32Fs, - high = BigInt(value) >> BinaryWriter.BIG_32 + const low = BigInt(value) & BIG_32Fs, high = BigInt(value) >> BIG_32 // write little endian number this.writeU32(Number(low)) @@ -164,8 +152,7 @@ export abstract class BinaryWriter implements Writer { } public writeU128(value: bigint | number) { - const low = BigInt(value) & BinaryWriter.BIG_64Fs, - high = BigInt(value) >> BinaryWriter.BIG_64 + const low = BigInt(value) & BIG_64Fs, high = BigInt(value) >> BIG_64 // write little endian number this.writeU64(low) @@ -173,29 +160,25 @@ export abstract class BinaryWriter implements Writer { } public writeI8(value: number) { - const bytes = 1 - this.ensureBufferWillHandleSize(bytes) - new DataView(this.buffer, this.offset).setInt8(0, value) - this.offset += bytes + this.alloc(1) + this.view.setInt8(this.offset, value) + this.offset += 1 } public writeI16(value: number) { - const bytes = 2 - this.ensureBufferWillHandleSize(bytes) - new DataView(this.buffer, this.offset).setInt16(0, value, true) - this.offset += bytes + this.alloc(2) + this.view.setInt16(this.offset, value, true) + this.offset += 2 } public writeI32(value: number) { - const bytes = 4 - this.ensureBufferWillHandleSize(bytes) - new DataView(this.buffer, this.offset).setInt32(0, value, true) - this.offset += bytes + this.alloc(4) + this.view.setInt32(this.offset, value, true) + this.offset += 4 } public writeI64(value: bigint | number) { - const low = BigInt(value) & BinaryWriter.BIG_32Fs, - high = BigInt(value) >> BinaryWriter.BIG_32 + const low = BigInt(value) & BIG_32Fs, high = BigInt(value) >> BIG_32 // write little endian number this.writeI32(Number(low)) @@ -203,8 +186,7 @@ export abstract class BinaryWriter implements Writer { } public writeI128(value: bigint | number) { - const low = BigInt(value) & BinaryWriter.BIG_64Fs, - high = BigInt(value) >> BinaryWriter.BIG_64 + const low = BigInt(value) & BIG_64Fs, high = BigInt(value) >> BIG_64 // write little endian number this.writeI64(low) @@ -227,17 +209,15 @@ export abstract class BinaryWriter implements Writer { } public writeF32(value: number) { - const bytes = 4 - this.ensureBufferWillHandleSize(bytes) - new DataView(this.buffer, this.offset).setFloat32(0, value, true) - this.offset += bytes + this.alloc(4) + this.view.setFloat32(this.offset, value, true) + this.offset += 4 } public writeF64(value: number) { - const bytes = 8 - this.ensureBufferWillHandleSize(bytes) - new DataView(this.buffer, this.offset).setFloat64(0, value, true) - this.offset += bytes + this.alloc(8) + this.view.setFloat64(this.offset, value, true) + this.offset += 8 } public writeChar(_value: string) { @@ -245,26 +225,21 @@ export abstract class BinaryWriter implements Writer { } public getBytes() { - return new Uint8Array(this.buffer.slice(0, this.offset)) + return new Uint8Array(this.view.buffer).slice(0, this.offset) } } export abstract class BinaryReader implements Reader { - private static readonly BIG_32 = 32n - private static readonly BIG_64 = 64n private static readonly TEXT_DECODER = new TextDecoder() - public buffer: ArrayBuffer public offset = 0 + public view: DataView constructor(data: Uint8Array) { // copies data to prevent outside mutation of buffer. - this.buffer = new ArrayBuffer(data.length) - new Uint8Array(this.buffer).set(data, 0) - } - - private read(length: number) { - return this.buffer.slice(this.offset, (this.offset = this.offset + length)) + const buffer = new ArrayBuffer(data.length) + new Uint8Array(buffer).set(data, 0) + this.view = new DataView(buffer) } abstract readLength(): number @@ -272,19 +247,14 @@ export abstract class BinaryReader implements Reader { abstract checkThatKeySlicesAreIncreasing(key1: [number, number], key2: [number, number]): void public readString() { - return BinaryReader.TEXT_DECODER.decode(this.readBytes()) - } - - public readBytes() { - const len = this.readLength() - if (len < 0) { - throw new Error("Length of a bytes array can't be negative") - } - return new Uint8Array(this.read(len)) + const length = this.readLength() + const decoded = BinaryReader.TEXT_DECODER.decode(new Uint8Array(this.view.buffer, this.offset, length)) + this.offset += length + return decoded } public readBool() { - return new Uint8Array(this.read(1))[0] === 1 + return this.readU8() === 1 } public readUnit() { @@ -292,51 +262,63 @@ export abstract class BinaryReader implements Reader { } public readU8() { - return new DataView(this.read(1)).getUint8(0) + const value = this.view.getUint8(this.offset) + this.offset += 1 + return value } public readU16() { - return new DataView(this.read(2)).getUint16(0, true) + const value = this.view.getUint16(this.offset, true) + this.offset += 2 + return value } public readU32() { - return new DataView(this.read(4)).getUint32(0, true) + const value = this.view.getUint32(this.offset, true) + this.offset += 4 + return value } public readU64() { const low = this.readU32(), high = this.readU32() // combine the two 32-bit values and return (little endian) - return (BigInt(high) << BinaryReader.BIG_32) | BigInt(low) + return (BigInt(high) << BIG_32) | BigInt(low) } public readU128() { const low = this.readU64(), high = this.readU64() // combine the two 64-bit values and return (little endian) - return (high << BinaryReader.BIG_64) | low + return (high << BIG_64) | low } public readI8() { - return new DataView(this.read(1)).getInt8(0) + const value = this.view.getInt8(this.offset) + this.offset += 1 + return value } public readI16() { - return new DataView(this.read(2)).getInt16(0, true) + const value = this.view.getInt16(this.offset, true) + this.offset += 2 + return value } public readI32() { - return new DataView(this.read(4)).getInt32(0, true) + const value = this.view.getInt32(this.offset, true) + this.offset += 4 + return value } public readI64() { const low = this.readI32(), high = this.readI32() // combine the two 32-bit values and return (little endian) - return (BigInt(high) << BinaryReader.BIG_32) | BigInt(low) + return (BigInt(high) << BIG_32) | BigInt(low) } public readI128() { const low = this.readI64(), high = this.readI64() // combine the two 64-bit values and return (little endian) - return (high << BinaryReader.BIG_64) | low + return (high << BIG_64) | low } public readOptionTag = this.readBool @@ -367,10 +349,14 @@ export abstract class BinaryReader implements Reader { } public readF32() { - return new DataView(this.read(4)).getFloat32(0, true) + const value = this.view.getFloat32(this.offset, true) + this.offset += 4 + return value } public readF64() { - return new DataView(this.read(8)).getFloat64(0, true) + const value = this.view.getFloat64(this.offset, true) + this.offset += 8 + return value } } diff --git a/suite/typescript/readme.md b/suite/typescript/readme.md index a9970f58c..6d594dd1a 100644 --- a/suite/typescript/readme.md +++ b/suite/typescript/readme.md @@ -10,21 +10,21 @@ gen:proto && gen:bincode && run:test && run:benchmarks ## Results ### Encode ``` -┌─────────┬───────────────────────────────┬─────────────┬───────────────────┬──────────┬─────────┐ -│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ -├─────────┼───────────────────────────────┼─────────────┼───────────────────┼──────────┼─────────┤ -│ 0 │ 'JSON:encode' │ '416,024' │ 2403.70173186696 │ '±0.43%' │ 416025 │ -│ 1 │ 'protobuf-js-ts-proto:encode' │ '1,032,930' │ 968.1190244070607 │ '±0.57%' │ 1032931 │ -│ 2 │ 'serdegen-bincode:encode' │ '163,409' │ 6119.603714582189 │ '±0.15%' │ 163410 │ -└─────────┴───────────────────────────────┴─────────────┴───────────────────┴──────────┴─────────┘ +┌─────────┬───────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼───────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'serdegen-bincode:encode' │ '303,315' │ 3296.893675242794 │ '±0.35%' │ 454974 │ +│ 1 │ 'JSON:encode' │ '457,023' │ 2188.0700444621666 │ '±0.12%' │ 685536 │ +│ 2 │ 'protobuf-js-ts-proto:encode' │ '1,022,729' │ 977.7752381694245 │ '±0.43%' │ 1534095 │ +└─────────┴───────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘ ``` ### Decode ``` ┌─────────┬───────────────────────────────┬───────────┬────────────────────┬──────────┬─────────┐ │ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ ├─────────┼───────────────────────────────┼───────────┼────────────────────┼──────────┼─────────┤ -│ 0 │ 'JSON:decode' │ '533,956' │ 1872.8128856825654 │ '±1.30%' │ 533957 │ -│ 1 │ 'protobuf-js-ts-proto:decode' │ '817,807' │ 1222.7815758713145 │ '±0.24%' │ 817808 │ -│ 2 │ 'serdegen-bincode:decode' │ '38,061' │ 26273.294650832962 │ '±0.58%' │ 38062 │ +│ 0 │ 'serdegen-bincode:decode' │ '717,551' │ 1393.628269449653 │ '±0.42%' │ 1076328 │ +│ 1 │ 'JSON:decode' │ '539,225' │ 1854.5121130414666 │ '±0.33%' │ 808839 │ +│ 2 │ 'protobuf-js-ts-proto:decode' │ '795,607' │ 1256.900927759333 │ '±0.19%' │ 1193412 │ └─────────┴───────────────────────────────┴───────────┴────────────────────┴──────────┴─────────┘ ``` \ No newline at end of file