Skip to content

Commit

Permalink
cache DataView to improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
MrFoxPro committed Sep 13, 2024
1 parent dd05a75 commit 6c2f861
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 109 deletions.
184 changes: 85 additions & 99 deletions serde-generate/runtime/typescript/serde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export type WrapperOfCase<T extends { $: string }, K = T["$"]> = T extends { $:

export interface Reader {
readString(): string
readBytes(): Uint8Array
readBool(): boolean
readUnit(): null
readChar(): string
Expand All @@ -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
Expand All @@ -76,135 +74,119 @@ 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
public writeUnit(_value: null) {
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))
this.writeU32(Number(high))
}

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)
this.writeU64(high)
}

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))
this.writeI32(Number(high))
}

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)
Expand All @@ -227,116 +209,116 @@ 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) {
throw new Error("Method serializeChar not implemented.")
}

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
abstract readVariantIndex(): number
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() {
return null
}

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
Expand Down Expand Up @@ -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
}
}
20 changes: 10 additions & 10 deletions suite/typescript/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
└─────────┴───────────────────────────────┴───────────┴────────────────────┴──────────┴─────────┘
```

0 comments on commit 6c2f861

Please sign in to comment.