Skip to content

Commit

Permalink
v1.7.4 mark sideEffects=false, fix f16round() overflow
Browse files Browse the repository at this point in the history
  • Loading branch information
reececomo committed Aug 9, 2024
1 parent 9f8a910 commit 4f97080
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 384 deletions.
2 changes: 1 addition & 1 deletion dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.cjs.map

Large diffs are not rendered by default.

498 changes: 254 additions & 244 deletions dist/index.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.mjs.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tinybuf",
"version": "1.7.3",
"version": "1.7.4",
"author": "Reece Como <[email protected]>",
"authors": [
"Reece Como <[email protected]>",
Expand All @@ -12,8 +12,9 @@
"module": "dist/index.mjs",
"types": "./dist/index.d.ts",
"files": ["./dist"],
"sideEffects": false,
"scripts": {
"build": "rimraf dist && rollup -c --bundleConfigAsCjs && rimraf dist/core && ./node_modules/.bin/dts-bundle-generator -o dist/index.d.ts src/index.ts --sort --no-banner --export-referenced-types",
"build": "rimraf dist && rollup -c --bundleConfigAsCjs && rimraf dist/core && ./node_modules/.bin/dts-bundle-generator -o dist/index.d.ts src/index.ts --no-banner --export-referenced-types",
"coverage": "jest --collectCoverage",
"lint": "eslint src --ext .ts",
"test": "jest"
Expand All @@ -23,14 +24,16 @@
"url": "git+https://github.com/reececomo/tinybuf.git"
},
"keywords": [
"buffer",
"arraybuffer",
"binary",
"parse",
"encode",
"buffer",
"decode",
"json",
"encode",
"js-binary",
"json",
"parse",
"ts-binary",
"typedarray",
"typescript-binary"
],
"devDependencies": {
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/BufferFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ describe('BufferFormat', () => {
expect(BufferFormat.peekHeaderStr(data)).toBe('AB');
});

it('should work', () => {
const myFormat = defineFormat('Ha', {
$exampleA: optional(Type.String),
$b: {
$c: Type.UInt,
$d: Type.UInt
},
});

myFormat.encode({ $exampleA: undefined, $b: { $c: 2, $d: 1 }});
});

it('should throw TypeError when passed an invalid header', () => {
expect(() => defineFormat(true as any, { data: Type.UInt })).toThrow(TypeError);
expect(() => defineFormat(-1, { data: Type.UInt })).toThrow(TypeError);
Expand Down
142 changes: 138 additions & 4 deletions src/__tests__/float16.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,141 @@
import { fround16 } from "../core/lib/float16";
import { f16round, $f16mask, $f16unmask } from "../core/lib/float16";

describe('fround16', () => {
it('should quantize numbers to the nearest float16 representation', () => {
expect(fround16(3.1419)).toBe(3.142578125);
describe('f16round', () => {
it('rounds to the nearest float16 for a given native float', () => {
expect(f16round(3.1419)).toBe(3.142578125);
expect(typeof f16round(3.1419)).toBe('number');
});

it('gives a close approximation (roughly ±0.01) for numbers below 1.0', () => {
expect(f16round(0.0123456789123456)).toBe(0.0123443603515625);
expect(f16round(0.0026789123456732)).toBe(0.0026798248291015625);
expect(f16round(0.4512312351232131)).toBe(0.451171875);
});

it('gives a close approximation (roughly ±0.25) for numbers between 1.0 and ~1024', () => {
expect(f16round(-99.123456789)).toBe(-99.125);
expect(f16round(512.333901298)).toBe(512.5);
expect(f16round(938.712391412)).toBe(938.5);
expect(f16round(-1_023.248888)).toBe(-1_023);
expect(f16round(-1_023.249999)).toBe(-1_023.5);
});

it('gives a reasonable approximatation (roughly ±1.0) for between ~1024 and ~4096', () => {
expect(f16round(2_234.523412)).toBe(2_234);
expect(f16round(-2_234.52341)).toBe(-2_234);
expect(f16round(3_431.646731)).toBe(3_432);
expect(f16round(-3_431.64672)).toBe(-3_432);
expect(f16round(4_096.599433)).toBe(4_096);
});

it('gives a lossy representation (roughly ±10.0) for numbers between ~4096 and 65504', () => {
expect(f16round(15_123.12304)).toBe(15_120);
expect(f16round(-15_123.1230)).toBe(-15_120);
expect(f16round(-64_065.25)).toBe(-64_064);
expect(f16round(64_123.25)).toBe(64_128);
});

it('overflows to Infinity, -Infinity for numbers beyond ±65,504', () => {
expect(f16round(65_503.999999999898)).toBe(65_504);
expect(f16round(-65_503.999999999898)).toBe(-65_504);


// Overflow to infinity
expect(f16round(65_505)).toBe(Infinity);
expect(f16round(65_505)).toBe(Infinity);
expect(f16round(65_518.12121212)).toBe(Infinity);
expect(f16round(65_520.89898989)).toBe(Infinity);
expect(f16round(-65_520.89898989)).toBe(-Infinity);
expect(f16round(65_555.123)).toBe(Infinity);
expect(f16round(-65_555.123)).toBe(-Infinity);
});
});

describe('$f16mask', () => {
it('should return a 16-bit bitmask', () => {
// 65,504 upper bound
expect($f16mask(-65_504)).toBe(0b00000000000000001111101111111111);
expect($f16mask(-65_504)).toBe(0b1111101111111111);

expect(asUint16Str($f16mask(-65_504))).toBe('1111101111111111');
expect(asInt32Str($f16mask(-65_504))).toBe('00000000000000001111101111111111');
expect(asUint32Str($f16mask(-65_504))).toBe('00000000000000001111101111111111');
expect(asInt64Str(BigInt($f16mask(-65_504)))).toBe('0000000000000000000000000000000000000000000000001111101111111111');
});

it('should NOT equal its own raw float64 representation', () => {
expect($f16mask(65_504)).not.toBe(65_504);
});
});

describe('$f16unmask', () => {
it('should read a 16-bit mask', () => {
// 65,504 upper bound
expect($f16unmask(0b1111_1011_1111_1111)).toBe(-65_504);
expect($f16unmask(0b0000_0000_0000_0000_1111_1011_1111_1111)).toBe(-65_504);
});

it('should survive a 16-bit mask', () => {
// 65,504 upper bound
expect($f16unmask(0b1111_1011_1111_1111)).toBe(-65_504);
expect($f16unmask(0b1111_1011_1111_1111_1111_1011_1111_1111)).toBe(-65_504);
});
});

test('mask / unmask basic compatibility', () => {
// this is covered pretty comprehensively in other tests
// including the coders - but just a quick sanity check here:
expect($f16mask(1023.5)).not.toBe(1023.5);
expect($f16unmask(1023.5)).not.toBe(1023.5);
expect($f16unmask($f16mask(1023.5))).toBe(1023.5);
});

test('performance benchmark: f16round() faster than native Math.fround()', () => {
const iterations = 1_000_000;
const inputs = Array.from({ length: iterations }, () => Math.random() > 0.5
? getRandomFloat(-1, 1) // mix small floats (e.g. vector normals)
: getRandomFloat(-70_000, 70_000) // and larger floats
);

// native Math.fround() - float32s:
const f32start = performance.now();
for (let i = 0; i < iterations; i++) {
Math.fround(inputs[i]);
}
const f32end = performance.now();
const f32duration = f32end - f32start;

// f16round() - float16s:
const f16start = performance.now();
for (let i = 0; i < iterations; i++) {
f16round(inputs[i]);
}
const f16end = performance.now();
const f16duration = f16end - f16start;

// generally 5-10x faster
// show soft warning if unexpectedly slower
try {
expect(f16duration).toBeLessThan(f32duration);
// console.debug(`result: f16round() (${f16duration.toFixed(3)}ms) vs Math.fround() (${f32duration.toFixed(3)}ms) for ${iterations} iterations`);
}
catch (error) {
console.warn(`f16round() (${f16duration.toFixed(3)}ms) vs Math.fround() (${f32duration.toFixed(3)}ms) for ${iterations} iterations`);
}
});

function asUint16Str(val: number): string {
return new Uint16Array([val])[0].toString(2).padStart(16, '0');
}
function asUint32Str(val: number): string {
return new Uint32Array([val])[0].toString(2).padStart(32, '0');
}
function asInt32Str(val: number): string {
return new Int32Array([val])[0].toString(2).padStart(32, '0');
}
function asInt64Str(val: bigint): string {
return new BigInt64Array([val])[0].toString(2).padStart(64, '0');
}
function getRandomFloat(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
2 changes: 1 addition & 1 deletion src/core/BufferFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class BufferFormat<EncoderType extends EncoderDefinition, HeaderType exte
) {
// set definition
if (def instanceof OptionalType) {
throw new TypeError("Invalid encoding format: Root object cannot be optionals.");
throw new TypeError("Invalid encoding format: Root object cannot be optional.");
}
else if (def !== undefined && typeof def === 'number') {
this._$type = def;
Expand Down
20 changes: 14 additions & 6 deletions src/core/Type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Binary coder types.
* Field types for defining encoding formats.
*
* @see {ValueTypes} for corresponding type definitions
* @see [Get started: Types](https://github.com/reececomo/tinybuf/blob/main/docs/get_started.md#types)
*/
export const enum Type {
/**
Expand All @@ -25,9 +25,9 @@ export const enum Type {
/**
* Signed integer (1 - 8 bytes).
* - 0 → ±64 = 1 byte
* - → ±8,192 = 2 bytes
* - → ±268,435,456 = 4 bytes
* - → ±`Number.MAX_SAFE_INTEGER` = 8 bytes
* - ±65 → ±8,192 = 2 bytes
* - ±8,193 → ±268,435,456 = 4 bytes
* - ±268,435,457 → ±`Number.MAX_SAFE_INTEGER` = 8 bytes
*/
Int,

Expand All @@ -46,7 +46,15 @@ export const enum Type {
/** Floating-point number (32-bit, single precision, 4 bytes). */
Float32,

/** Floating-point number (16-bit, half precision, 2 bytes). */
/**
* Floating-point number (16-bit, half precision, 2 bytes).
*
* **Warning:** ±4,096Maximum value: ±65,504.
*
* Reasonable precision between -1024 and 1024.
*
* @see `f16round()` for `Math.fround()` equivalent.
*/
Float16,

/** A signed scalar between -1.00 and 1.00 (1 byte). */
Expand Down
48 changes: 27 additions & 21 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
const MTU = 1500;

/** Set Tinybuf global config */
export const setTinybufConfig = (newSettings: Partial<typeof cfg>): void => {
cfg = {
...cfg,
...newSettings
};
export const setTinybufConfig = (c: Partial<TinybufConfig>): void => {
cfg = { ...cfg, ...c };
};

export let cfg = {
export type TinybufConfig = {
/**
* (default: false)
* By default `BufferFormat.encode(…)` optimizes performance and memory by
Expand All @@ -21,37 +18,46 @@ export let cfg = {
*
* Set `safe` to true to copy bytes to a new buffer and return that.
*/
safe: false,
safe: boolean,

/**
* (default: true)
* By default, format encoders share a global encoding buffer for performance
* and memory management reasons.
*
* When set to false, each format will be allocated its own resizable
* encoding buffer.
*
* Enable to maximise performance and memory re-use, just be cautious of
* possible race conditions.
*/
useGlobalEncodingBuffer: boolean,

/**
* (default: 1500)
* The maximum bytes to allocate to an encoding buffer. If using the global
* encoding buffer, this is the size it is initialized to.
*/
encodingBufferMaxSize: MTU,
encodingBufferMaxSize: number,

/**
* (default: 256)
* Initial bytes to allocate to individual format encoding buffers, if used.
*/
encodingBufferInitialSize: 256,
encodingBufferInitialSize: number,

/**
* (default: 256)
* Additional bytes when resizing individual format encoding buffers, if used.
*/
encodingBufferIncrement: 256,
encodingBufferIncrement: number,
};

/**
* (default: true)
* By default, format encoders share a global encoding buffer for performance
* and memory management reasons.
*
* When set to false, each format will be allocated its own resizable
* encoding buffer.
*
* Enable to maximise performance and memory re-use, just be cautious of
* possible race conditions.
*/
/** @internal */
export let cfg: TinybufConfig = {
safe: false,
useGlobalEncodingBuffer: true,
encodingBufferMaxSize: MTU,
encodingBufferInitialSize: 256,
encodingBufferIncrement: 256,
};
Loading

0 comments on commit 4f97080

Please sign in to comment.