From f9419bb4bf38e14be7fd60145a8175a3dca9d55d Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Sun, 22 Oct 2017 12:33:47 +0200 Subject: [PATCH] collections --- src/perf-tests/chunked-array-vs-native.ts | 44 ++++++ src/structure/collections/ordered-set.ts | 27 ++-- src/utils/chunked-array.ts | 170 +++++++++++----------- 3 files changed, 142 insertions(+), 99 deletions(-) create mode 100644 src/perf-tests/chunked-array-vs-native.ts diff --git a/src/perf-tests/chunked-array-vs-native.ts b/src/perf-tests/chunked-array-vs-native.ts new file mode 100644 index 000000000..3760b7f3a --- /dev/null +++ b/src/perf-tests/chunked-array-vs-native.ts @@ -0,0 +1,44 @@ +import * as B from 'benchmark' +import ChunkedArray from '../utils/chunked-array' + +function testNative(size: number) { + const xs = new Array(size); + for (let i = 0; i < size; i++) xs[i] = i * i; + return xs; +} + +function testChunkedTyped(size: number, chunk: number, linear: boolean) { + const xs = ChunkedArray.create(s => new Int32Array(s), 1, chunk, linear); + for (let i = 0; i < size; i++) ChunkedArray.add(xs, i * i); + return ChunkedArray.compact(xs); +} + +function testChunkedNative(size: number, chunk: number) { + const xs = ChunkedArray.create(s => [], 1, chunk, false); + for (let i = 0; i < size; i++) ChunkedArray.add(xs, i * i); + return ChunkedArray.compact(xs); +} + +const suite = new B.Suite(); + +const N = 70000; + +suite + .add('native', () => testNative(N)) + .add('chunkedT 0.1k', () => testChunkedTyped(N, 100, false)) + .add('chunkedT 4k', () => testChunkedTyped(N, 4096, false)) + .add('chunkedT 4k lin', () => testChunkedTyped(N, 4096, true)) + .add('chunkedT N / 2', () => testChunkedTyped(N, N / 2, false)) + .add('chunkedT N', () => testChunkedTyped(N, N, false)) + .add('chunkedT 2 * N', () => testChunkedTyped(N, 2 * N, false)) + + .add('chunkedN N', () => testChunkedNative(N, N)) + .add('chunkedN 0.1k', () => testChunkedNative(N, 100)) + .add('chunkedN N / 2', () => testChunkedNative(N, N / 2)) + .add('chunkedN 2 * N', () => testChunkedNative(N, 2 * N)) + .on('cycle', (e: any) => { + console.log(String(e.target)); + }) + .run(); + +//console.log(testChunkedTyped(10, 16)); diff --git a/src/structure/collections/ordered-set.ts b/src/structure/collections/ordered-set.ts index 67e3e3490..38b5e33c8 100644 --- a/src/structure/collections/ordered-set.ts +++ b/src/structure/collections/ordered-set.ts @@ -24,11 +24,6 @@ class RangeImpl implements Impl { size: number; has(x: number) { return x >= this.min && x <= this.max; } indexOf(x: number) { return x >= this.min && x <= this.max ? x - this.min : -1; } - toArray() { - const ret = new Array(this.size); - for (let i = 0; i < this.size; i++) ret[i] = i + this.min; - return ret; - } elementAt(i: number) { return this.min + i; } elements() { return Iterator.Range(this.min, this.max); } @@ -54,6 +49,17 @@ class ArrayImpl implements Impl { } namespace OrderedSet { + export function ofSingleton(value: number): OrderedSet { return new RangeImpl(value, value); } + export function ofRange(min: number, max: number): OrderedSet { return max < min ? Empty : new RangeImpl(min, max); } + /** It is the responsibility of the caller to ensure the array is sorted and contains unique values. */ + export function ofSortedArray(xs: ArrayLike<number>): OrderedSet { + if (!xs.length) return Empty; + // check if the array is just a range + if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return ofRange(xs[0], xs[xs.length - 1]); + return new ArrayImpl(xs); + } + export const Empty = new RangeImpl(0, -1); + export function isEmpty(a: OrderedSet) { return a.size === 0; } export function hashCode(a: OrderedSet) { @@ -117,17 +123,6 @@ namespace OrderedSet { return intersectAA((a as ArrayImpl).values, (b as ArrayImpl).values); } } - - export function ofSingleton(value: number): OrderedSet { return new RangeImpl(value, value); } - export function ofRange(min: number, max: number): OrderedSet { return max < min ? Empty : new RangeImpl(min, max); } - /** It is the responsibility of the caller to ensure the array is sorted and contains unique values. */ - export function ofSortedArray(xs: ArrayLike<number>): OrderedSet { - if (!xs.length) return Empty; - // check if the array is just a range - if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return ofRange(xs[0], xs[xs.length - 1]); - return new ArrayImpl(xs); - } - export const Empty = new RangeImpl(0, -1); } function min(a: OrderedSet) { return (a as Impl).min; } diff --git a/src/utils/chunked-array.ts b/src/utils/chunked-array.ts index fa1459e6a..3ce84f5cd 100644 --- a/src/utils/chunked-array.ts +++ b/src/utils/chunked-array.ts @@ -9,125 +9,129 @@ * A generic chunked array builder. * * When adding elements, the array growns by a specified number - * of elements and no copying is done until ChunkedArray.compact - * is called. + * of elements (either linear or exponential growth) and no copying + * is done until ChunkedArray.compact is called. */ -export interface ChunkedArray<T> { - creator: (size: number) => any; - elementSize: number; - chunkSize: number; - current: any; - currentIndex: number; - - parts: any[]; - elementCount: number; +interface ChunkedArray<T> { + ctor: (size: number) => any, + elementSize: number, + + linearGrowth: boolean, + + initialSize: number, + allocatedSize: number, + elementCount: number, + + currentSize: number, + currentChunk: any, + currentIndex: number, + + chunks: any[] } -export namespace ChunkedArray { +// TODO: better api, write tests +namespace ChunkedArray { export function is(x: any): x is ChunkedArray<any> { return x.creator && x.chunkSize; } - export function add4<T>(array: ChunkedArray<T>, x: T, y: T, z: T, w: T) { - if (array.currentIndex >= array.chunkSize) { - array.currentIndex = 0; - array.current = array.creator(array.chunkSize); - array.parts[array.parts.length] = array.current; - } + function allocateNext(array: ChunkedArray<any>) { + let nextSize = !array.allocatedSize || array.linearGrowth + ? array.initialSize * array.elementSize + : Math.max(Math.ceil(0.61 * array.allocatedSize), 1); + if (nextSize % array.elementSize !== 0) nextSize += nextSize % array.elementSize; + array.currentSize = nextSize; + array.currentIndex = 0; + array.currentChunk = array.ctor(nextSize); + array.allocatedSize += nextSize; + array.chunks[array.chunks.length] = array.currentChunk; + } - array.current[array.currentIndex++] = x; - array.current[array.currentIndex++] = y; - array.current[array.currentIndex++] = z; - array.current[array.currentIndex++] = w; + export function add4<T>(array: ChunkedArray<T>, x: T, y: T, z: T, w: T) { + if (array.currentIndex >= array.currentSize) allocateNext(array); + const c = array.currentChunk; + c[array.currentIndex++] = x; + c[array.currentIndex++] = y; + c[array.currentIndex++] = z; + c[array.currentIndex++] = w; return array.elementCount++; } export function add3<T>(array: ChunkedArray<T>, x: T, y: T, z: T) { - if (array.currentIndex >= array.chunkSize) { - array.currentIndex = 0; - array.current = array.creator(array.chunkSize); - array.parts[array.parts.length] = array.current; - } - - array.current[array.currentIndex++] = x; - array.current[array.currentIndex++] = y; - array.current[array.currentIndex++] = z; + if (array.currentIndex >= array.currentSize) allocateNext(array); + const c = array.currentChunk; + c[array.currentIndex++] = x; + c[array.currentIndex++] = y; + c[array.currentIndex++] = z; return array.elementCount++; } export function add2<T>(array: ChunkedArray<T>, x: T, y: T) { - if (array.currentIndex >= array.chunkSize) { - array.currentIndex = 0; - array.current = array.creator(array.chunkSize); - array.parts[array.parts.length] = array.current; - } - - array.current[array.currentIndex++] = x; - array.current[array.currentIndex++] = y; + if (array.currentIndex >= array.currentSize) allocateNext(array); + const c = array.currentChunk; + c[array.currentIndex++] = x; + c[array.currentIndex++] = y; return array.elementCount++; } export function add<T>(array: ChunkedArray<T>, x: T) { - if (array.currentIndex >= array.chunkSize) { - array.currentIndex = 0; - array.current = array.creator(array.chunkSize); - array.parts[array.parts.length] = array.current; - } - - array.current[array.currentIndex++] = x; + if (array.currentIndex >= array.currentSize) allocateNext(array); + array.currentChunk[array.currentIndex++] = x; return array.elementCount++; } - export function compact<T>(array: ChunkedArray<T>): T[] { - const ret = array.creator(array.elementSize * array.elementCount) - const offset = (array.parts.length - 1) * array.chunkSize - let offsetInner = 0 - let part: any + export function compact<T>(array: ChunkedArray<T>): ArrayLike<T> { + const { ctor, chunks, currentIndex } = array; - if (array.parts.length > 1) { - if (array.parts[0].buffer) { - for (let i = 0; i < array.parts.length - 1; i++) { - ret.set(array.parts[i], array.chunkSize * i); - } - } else { + if (!chunks.length) return ctor(0); + if (chunks.length === 1 && currentIndex === array.allocatedSize) { + return chunks[0]; + } - for (let i = 0; i < array.parts.length - 1; i++) { - offsetInner = array.chunkSize * i; - part = array.parts[i]; + const ret = ctor(array.elementSize * array.elementCount); + let offset = 0; - for (let j = 0; j < array.chunkSize; j++) { - ret[offsetInner + j] = part[j]; - } - } + if (ret.buffer) { + for (let i = 0, _i = chunks.length - 1; i < _i; i++) { + ret.set(chunks[i], offset); + offset += chunks[i].length; } - } - - if (array.current.buffer && array.currentIndex >= array.chunkSize) { - ret.set(array.current, array.chunkSize * (array.parts.length - 1)); } else { - for (let i = 0; i < array.currentIndex; i++) { - ret[offset + i] = array.current[i]; + for (let i = 0, _i = chunks.length - 1; i < _i; i++) { + const chunk = chunks[i]; + for (let j = 0, _j = chunk.length; j < _j; j++) ret[offset + j] = chunk[j]; + offset += chunk.length; } } - return ret as any; - } - export function create<T>(creator: (size: number) => any, chunkElementCount: number, elementSize: number): ChunkedArray<T> { - chunkElementCount = chunkElementCount | 0; - if (chunkElementCount <= 0) chunkElementCount = 1; + const lastChunk = chunks[chunks.length - 1]; + if (ret.buffer && currentIndex >= array.currentSize) { + ret.set(lastChunk, offset); + } else { + for (let j = 0, _j = lastChunk.length; j < _j; j++) ret[offset + j] = lastChunk[j]; + } - let chunkSize = chunkElementCount * elementSize; - let current = creator(chunkSize) + return ret; + } + export function create<T>(ctor: (size: number) => any, elementSize: number, initialSize: number, linearGrowth: boolean): ChunkedArray<T> { return { + ctor, elementSize, - chunkSize, - creator, - current, - parts: [current], + linearGrowth, + + initialSize, + allocatedSize: 0, + elementCount: 0, + + currentSize: 0, + currentChunk: void 0, currentIndex: 0, - elementCount: 0 - } as ChunkedArray<T> + + chunks: [] + } as ChunkedArray<T>; } } + +export default ChunkedArray \ No newline at end of file -- GitLab