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