Linked Lists: Part 3
Continuing in the series on linked lists that started with parts one and two, today I’ll make the first serious optimization pass on the LinkedList implementation. Read on for how successful this is.
Firstly, while I claimed last time that the API was complete, I changed it slightly this time. I realized that the two Array constructors (size and arguments) were actually one constructor. This allowed me to eliminate the fromValues static function and merge it into the LinkedList constructor. Other than this, the API remains stable. I also added some safety to the join function by handling the empty list case. Now, let’s look function-by-function at this round of optimization:
Function | Optimization |
---|---|
Constructor/fromValues | Inlined and specialized push calls. |
elementAt | Used an approach similar to Duff’s Device to minimize iteration and checking with if. |
concat | Inlined and specialized push calls. |
filter | Inlined and specialized push calls. |
join | Handled empty list case (a minor performance decrease). |
map | Inlined and specialized push calls. |
push | Reorganized around a temporary newNode variable. |
slice | Inlined and specialized push calls. |
splice | Inlined and specialized push calls. |
toString | Inlined join. |
toLocaleString | Inlined join. |
unshift | Reorganized around a temporary newNode variable. |
toArray | Pre-allocated array. Inlined and specialized push calls. |
fromArray | Inlined and specialized push calls. |
As you can see from the table, most of the work involved inlining calls to push Function calls are quite slow in AS3 and var args functions like push are especially slow. So eliminating them became a natural first step in the optimization effort. It also allows some specialization of the function to the unique circumstances in which it’s being used. For example, there’s no need for the var args loop anymore since we are always pushing a single value. This eliminates another slow item: array access. The join method was also inlined in toString and toLocaleString, but I haven’t even been testing those methods as they (unlike join) are not frequently used in performance-critical areas. In unshift and push I created a newNode temporary variable outside of the loop and used it to get around a lot of field access. While field access is usually a minor concern, it’s still more expensive than accessing the local newNode temporary variable.
Definitely the most obscure optimization here is in elementAt. We saw last time that indexOf was taking a horrendous amount of time compared to its Array equivalent: the lowly [] operator. This slowness is fundamental to linked lists because of the need to “walk” the list while counting until the index is reached. If you need to do lots of random access (ie. elementAt/[]), you should probably not be using a linked list in the first place. However, we should still try to minimize the pain for those cases where usage is light and a linked list is still desirable due to its benefits in other areas. The really expensive part of the old elementAt implementation was the if check at every single iteration. I recalled reading about Duff’s Device before and, while it seems truly strange, figured that a similar approach could be used here. The idea is to check a several of the upcoming elements every iteration rather than just the current one. A switch is handy here and can be expanded by simple copy/paste to increase or decrease the lookahead range; I settled on 10 elements as diminishing returns set in.
So, let’s have a look at the new code:
LinkedListNode
No changes.
package { /** * A node in a linked list. Its purpose is to hold the data in the * node as well as links to the previous and next nodes. * @author Jackson Dunstan */ public class LinkedListNode { public var next:LinkedListNode; public var prev:LinkedListNode; public var data:*; public function LinkedListNode(data:*=undefined) { this.data = data; } } }
LinkedList
Optimizations, an API change, and a bug fix as mentioned above.
package { /** * A linked list, which is a single-dimensional chain of objects called * nodes. This implementation is doubly-linked, so each node has a link * to the next and previous node. It's API is designed to mimic that of * the top-level Array class. * @author Jackson Dunstan */ public class LinkedList { public var head:LinkedListNode; public var tail:LinkedListNode; public var length:int; public function LinkedList(...values) { var len:int = this.length = values.length; var head:LinkedListNode = null; var newNode:LinkedListNode; var i:int; // Equivalent to Array(len) if (len == 1) { len = values[0]; head = this.tail = newNode = new LinkedListNode(); for (i = 1; i < len; ++i) { newNode = new LinkedListNode(); newNode.next = head; head.prev = newNode; head = newNode; } } // Equivalent to Array(value0, value1, ..., valueN) else if (len > 1) { i = len-1; head = this.tail = newNode = new LinkedListNode(values[i--]); for (; i >= 0; --i) { newNode = new LinkedListNode(values[i]); newNode.next = head; head.prev = newNode; head = newNode; } } this.head = head; } /** * Equivalent to the Array [] operator * @param index Index of the element to get * @return The element at the given index */ public function elementAt(index:int): * { if (index < 0) { throw new TypeError("Error #2007"); } else if (index >= this.length) { return undefined; } else { var halfLength:int = this.length >> 1; var cur:LinkedListNode; var i:int; // Element is in the first half, start at beginning if (index < halfLength) { i = 0; cur = this.head; while (true) { switch (index-i) { case 0: return cur.data; case 1: return cur.next.data; case 2: return cur.next.next.data; case 3: return cur.next.next.next.data; case 4: return cur.next.next.next.next.data; case 5: return cur.next.next.next.next.next.data; case 6: return cur.next.next.next.next.next.next.data; case 7: return cur.next.next.next.next.next.next.next.data; case 8: return cur.next.next.next.next.next.next.next.next.data; case 9: return cur.next.next.next.next.next.next.next.next.next.data; case 10: return cur.next.next.next.next.next.next.next.next.next.next.data; } cur = cur.next.next.next.next.next.next.next.next.next.next.next; i += 11; } } // Element is in the second half, start at the end else { i = this.length-1; cur = this.tail; while (true) { switch (i-index) { case 0: return cur.data; case 1: return cur.prev.data; case 2: return cur.prev.prev.data; case 3: return cur.prev.prev.prev.data; case 4: return cur.prev.prev.prev.prev.data; case 5: return cur.prev.prev.prev.prev.prev.data; case 6: return cur.prev.prev.prev.prev.prev.prev.data; case 7: return cur.prev.prev.prev.prev.prev.prev.prev.data; case 8: return cur.prev.prev.prev.prev.prev.prev.prev.prev.data; case 9: return cur.prev.prev.prev.prev.prev.prev.prev.prev.prev.data; case 10: return cur.prev.prev.prev.prev.prev.prev.prev.prev.prev.prev.data; } cur = cur.prev.prev.prev.prev.prev.prev.prev.prev.prev.prev.prev; i -= 11; } } } } public function concat(...args): LinkedList { var ret:LinkedList = new LinkedList(); var newNode:LinkedListNode; // Add everything from this list for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { newNode = new LinkedListNode(cur.data); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; } // Add everything from args var list:LinkedList; for each (var arg:* in args) { // Lists get flattened if (arg is LinkedList) { list = arg; for (cur = list.head; cur; cur = cur.next) { newNode = new LinkedListNode(cur.data); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; } } // No flattening for any other type, even Array else { newNode = new LinkedListNode(arg); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; } } return ret; } public function every(callback:Function, thisObject:*=null): Boolean { var index:int = 0; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { if (!callback.call(thisObject, cur.data, index, this)) { return false; } index++; } return true; } public function filter(callback:Function, thisObject:*=null): LinkedList { var ret:LinkedList = new LinkedList(); var index:int = 0; var newNode:LinkedListNode; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { if (callback.call(thisObject, cur.data, index, this)) { newNode = new LinkedListNode(cur.data); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; } index++; } return ret; } public function forEach(callback:Function, thisObject:*=null): void { var index:int = 0; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { callback.call(thisObject, cur.data, index, this); index++; } } public function indexOf(searchElement:*, fromIndex:int=0): int { var index:int = 0; for (var cur:LinkedListNode = this.head; cur && index < fromIndex; cur = cur.next) { index++; } for (; cur; cur = cur.next) { if (cur.data == searchElement) { return index; } } return -1; } public function join(sep:*=","): String { if (!this.head) { return ""; } var ret:String = ""; for (var curNode:LinkedListNode = this.head; curNode; curNode = curNode.next) { ret += curNode.data + sep; } return ret.substr(0, ret.length-sep.length); } public function lastIndexOf(searchElement:*, fromIndex:int=0x7fffffff): int { var index:int = this.length-1; for (var cur:LinkedListNode = this.tail; cur && index > fromIndex; cur = cur.prev) { index--; } for (; cur; cur = cur.prev) { if (cur.data == searchElement) { return index; } index--; } return -1; } public function map(callback:Function, thisObject:*=null): LinkedList { var ret:LinkedList = new LinkedList(); var index:int = 0; var newNode:LinkedListNode; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { newNode = new LinkedListNode(callback.call(thisObject, cur.data, index, this)); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; index++; } return ret; } public function pop(): * { if (this.tail) { var ret:* = this.tail.data; this.tail = this.tail.prev; this.length--; return ret; } else { return undefined; } } public function push(...args): void { var numArgs:int = args.length; var arg:*; var newNode:LinkedListNode; for (var i:int = 0; i < numArgs; ++i) { arg = args[i]; newNode = new LinkedListNode(arg); newNode.prev = this.tail; if (this.tail) { this.tail.next = newNode; } else { this.head = newNode; } this.tail = newNode; } this.length += numArgs; } public function reverse(): LinkedList { var front:LinkedListNode = this.head; var back:LinkedListNode = this.tail; var temp:*; while (front != back && back.prev != front) { temp = front.data; front.data = back.data; back.data = temp; front = front.next; back = back.prev; } return this; } public function shift(): * { if (this.head) { var ret:* = this.head.data; this.head = this.head.next; this.length--; return ret; } else { return undefined; } } public function slice(startIndex:int=0, endIndex:int=16777215): LinkedList { var ret:LinkedList = new LinkedList(); if (startIndex >= this.length || endIndex <= startIndex) { return ret; } var cur:LinkedListNode = this.head; var i:int; var newNode:LinkedListNode; for (i = 0; i < startIndex && cur; ++i) { cur = cur.next; } for (; i < endIndex && cur; ++i) { newNode = new LinkedListNode(cur.data); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; cur = cur.next; } return ret; } public function some(callback:Function, thisObject:*=null): Boolean { var index:int = 0; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { if (callback.call(thisObject, cur.data, index, this)) { return true; } index++; } return false; } public function sort(...args): LinkedList { var arr:Array = toArray(); return fromArray(arr.sort.apply(arr, args)); } public function sortOn(fieldName:Object, options:Object=null): LinkedList { var arr:Array = toArray(); return fromArray(arr.sortOn.call(arr, fieldName, options)); } public function splice(startIndex:int, deleteCount:int, ...values): LinkedList { var ret:LinkedList = new LinkedList(); var cur:LinkedListNode = this.head; var endIndex:int = startIndex + deleteCount; var i:int; var newNode:LinkedListNode; for (i = 0; i < startIndex && cur; ++i) { cur = cur.next; } for (; i < endIndex && cur; ++i) { // Push current node to spliced list newNode = new LinkedListNode(cur.data); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; // Unlink current node and move on cur.next = cur.next.next; cur.next.prev = cur; cur = cur.next; } this.length -= deleteCount; return ret; } public function toString(): String { if (!this.head) { return ""; } var ret:String = ""; for (var curNode:LinkedListNode = this.head; curNode; curNode = curNode.next) { ret += curNode.data + ","; } return ret.substr(0, ret.length-1); } public function toLocaleString(): String { if (!this.head) { return ""; } var ret:String = ""; for (var curNode:LinkedListNode = this.head; curNode; curNode = curNode.next) { ret += curNode.data + ","; } return ret.substr(0, ret.length-1); } public function unshift(...args): void { var numArgs:int = args.length; var arg:*; var newNode:LinkedListNode; for (var i:int = 0; i < numArgs; ++i) { arg = args[i]; newNode = new LinkedListNode(arg); newNode.next = this.head; if (this.head) { this.head.prev = newNode; } else { this.tail = newNode; } this.head = newNode; } this.length += numArgs; } private function toArray(): Array { var ret:Array = new Array(this.length); var i:int = 0; for (var cur:LinkedListNode = this.head; cur; cur = cur.next) { ret[i] = cur.data; i++; } return ret; } private function fromArray(arr:Array): LinkedList { var ret:LinkedList = new LinkedList(); var newNode:LinkedListNode; for each (var val:* in arr) { newNode = new LinkedListNode(val); newNode.prev = ret.tail; if (ret.tail) { ret.tail.next = newNode; } else { ret.head = newNode; } ret.tail = newNode; } return ret; } } }
LinkedListPerformance
No changes, even to the numbers of iterations.
package { import flash.display.*; import flash.events.*; import flash.text.*; import flash.utils.*; /** * An app to test linked list performance * @author Jackson Dunstan */ public class LinkedListPerformance extends Sprite { public function LinkedListPerformance() { // Setup logger var logger:TextField = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); function log(msg:*): void { logger.appendText(msg); } // Properties const VALUE:Object = {val:33}; const ALTERNATE_VALUE:Object = {val:44}; const SIZE:int = 10000; const SORT_ON_PROP_NAME:String = "val"; const ELEMENTATINDEX:int = SIZE >> 1; const SPLICE_START:int = 0; const SLICE_START:int = 00; const SLICE_END:int = SIZE; // Iterations const TRAVERSES:int = 100; const ELEMENTATS:int = 100000; const CONCATS:int = 10; const EVERYS:int = 10; const FILTERS:int = 10; const FOREACHS:int = 10; const INDEXOFS:int = 100; const JOINS:int = 1; const MAPS:int = 1; const PUSHPOPS:int = 100000; const REVERSES:int = 100; const SLICES:int = 100; const SOMES:int = 1; const SORTS:int = 1; const SORTONS:int = 1; const SPLICES:int = 10000; const UNSHIFTSHIFTS:int = 10000; // Test variables var array:Array; var list:LinkedList; var beforeTime:int; var curNode:LinkedListNode; var i:int; var j:int; // Populate the array array = []; for (i = 0; i < SIZE; ++i) { array[i] = VALUE; } // Populate the list list = new LinkedList(); for (i = 0; i < SIZE; ++i) { list.push(VALUE); } log("traverse (" + TRAVERSES + " operations): "); beforeTime = getTimer(); for (j = 0; j < TRAVERSES; ++j) { for (i = 0; i < SIZE; ++i) { array[i]; } } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (j = 0; j < TRAVERSES; ++j) { for (curNode = list.head; curNode; curNode = curNode.next) { curNode.data; } } log((getTimer()-beforeTime) + " list\n"); log("elementAt/[] (" + ELEMENTATS + " operations): "); beforeTime = getTimer(); for (i = 0; i < ELEMENTATS; ++i) { array[ELEMENTATINDEX]; } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < ELEMENTATS; ++i) { list.elementAt(ELEMENTATINDEX); } log((getTimer()-beforeTime) + " list\n"); log("concat (" + CONCATS + " operations): "); beforeTime = getTimer(); for (i = 0; i < CONCATS; ++i) { array.concat(array); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < CONCATS; ++i) { list.concat(list); } log((getTimer()-beforeTime) + " list\n"); log("every (" + EVERYS + " operations): "); beforeTime = getTimer(); for (i = 0; i < EVERYS; ++i) { array.every(arrayCallbackTrue); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < EVERYS; ++i) { list.every(listCallbackTrue); } log((getTimer()-beforeTime) + " list\n"); log("filter (" + FILTERS + " operations): "); beforeTime = getTimer(); for (i = 0; i < FILTERS; ++i) { array.filter(arrayCallbackTrue); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < FILTERS; ++i) { list.filter(listCallbackTrue); } log((getTimer()-beforeTime) + " list\n"); log("forEach (" + FOREACHS + " operations): "); beforeTime = getTimer(); for (i = 0; i < FOREACHS; ++i) { array.forEach(arrayCallbackTrue); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < FOREACHS; ++i) { list.forEach(listCallbackTrue); } log((getTimer()-beforeTime) + " list\n"); log("indexOf (" + INDEXOFS + " operations): "); array[SIZE-1] = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { array.indexOf(ALTERNATE_VALUE); } log((getTimer()-beforeTime) + " array/"); array[SIZE-1] = VALUE; list.tail.data = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { list.indexOf(ALTERNATE_VALUE); } log((getTimer()-beforeTime) + " list\n"); list.tail.data = VALUE; log("join (" + JOINS + " operations): "); beforeTime = getTimer(); for (i = 0; i < JOINS; ++i) { array.join(); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < JOINS; ++i) { list.join(); } log((getTimer()-beforeTime) + " list\n"); log("lastIndexOf (" + INDEXOFS + " operations): "); array[0] = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { array.lastIndexOf(ALTERNATE_VALUE); } log((getTimer()-beforeTime) + " array/"); array[0] = VALUE; list.head.data = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { list.lastIndexOf(ALTERNATE_VALUE); } log((getTimer()-beforeTime) + " list\n"); list.head.data = VALUE; log("map (" + MAPS + " operations): "); beforeTime = getTimer(); for (i = 0; i < MAPS; ++i) { array.map(arrayCallbackTrue); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < MAPS; ++i) { list.map(listCallbackTrue); } log((getTimer()-beforeTime) + " list\n"); log("pop (" + PUSHPOPS + " operations): "); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { array.pop(); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { list.pop(); } log((getTimer()-beforeTime) + " list\n"); log("push (" + PUSHPOPS + " operations): "); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { array.push(VALUE); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { list.push(VALUE); } log((getTimer()-beforeTime) + " list\n"); log("reverse (" + REVERSES + " operations): "); beforeTime = getTimer(); for (i = 0; i < REVERSES; ++i) { array.reverse(); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < REVERSES; ++i) { list.reverse(); } log((getTimer()-beforeTime) + " list\n"); log("shift (" + UNSHIFTSHIFTS + " operations): "); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { array.shift(); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { list.shift(); } log((getTimer()-beforeTime) + " list\n"); log("slice (" + SLICES + " operations): "); beforeTime = getTimer(); for (i = 0; i < SLICES; ++i) { array.slice(SLICE_START, SLICE_END); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < SLICES; ++i) { list.slice(SLICE_START, SLICE_END); } log((getTimer()-beforeTime) + " list\n"); log("some (" + SOMES + " operations): "); beforeTime = getTimer(); for (i = 0; i < SOMES; ++i) { array.some(arrayCallbackFalse); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < SOMES; ++i) { list.some(listCallbackFalse); } log((getTimer()-beforeTime) + " list\n"); log("sort (" + SORTS + " operations): "); beforeTime = getTimer(); for (i = 0; i < SORTS; ++i) { array.sort(); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < SORTS; ++i) { list.sort(); } log((getTimer()-beforeTime) + " list\n"); log("sortOn (" + SORTONS + " operations): "); beforeTime = getTimer(); for (i = 0; i < SORTONS; ++i) { array.sortOn(SORT_ON_PROP_NAME); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < SORTONS; ++i) { list.sortOn(SORT_ON_PROP_NAME); } log((getTimer()-beforeTime) + " list\n"); log("splice (" + SPLICES + " operations): "); beforeTime = getTimer(); for (i = 0; i < SPLICES; ++i) { array.splice(SPLICE_START, 5, VALUE, VALUE, VALUE, VALUE, VALUE); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < SPLICES; ++i) { list.splice(SPLICE_START, 5, VALUE, VALUE, VALUE, VALUE, VALUE); } log((getTimer()-beforeTime) + " list\n"); log("unshift (" + UNSHIFTSHIFTS + " operations): "); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { array.unshift(VALUE); } log((getTimer()-beforeTime) + " array/"); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { list.unshift(VALUE); } log((getTimer()-beforeTime) + " list\n"); } private function arrayCallbackTrue(cur:*, index:int, list:Array): Boolean { return true; } private function listCallbackTrue(cur:*, index:int, list:LinkedList): Boolean { return true; } private function arrayCallbackFalse(cur:*, index:int, list:Array): Boolean { return false; } private function listCallbackFalse(cur:*, index:int, list:LinkedList): Boolean { return false; } } }
Results
Function | Operations | 2.2 Ghz Intel Core 2 Duo, 2 GB RAM, Mac OS X 10.6 | Speedup |
---|---|---|---|
traverse | 100 | 12 array/14 linked list | 0.86 |
elementAt/[] | 100000 | 1 array/1567 linked list | 0.00 |
concat | 10 | 4 array/64 linked list | 0.06 |
every | 10 | 12 array/15 linked list | 0.8 |
filter | 10 | 16 array/47 linked list | 0.34 |
forEach | 10 | 12 array/14 linked list | 0.86 |
indexOf | 100 | 15 array/22 linked list | 0.68 |
join | 1 | 23 array/27 linked list | 0.85 |
lastIndexOf | 100 | 15 array/21 linked list | 0.71 |
map | 1 | 2 array/4 linked list | 0.5 |
pop | 100000 | 1 array/5 linked list | 0.2 |
push | 100000 | 7 array/86 linked list | 0.08 |
reverse | 100 | 21 array/141 linked list | 0.15 |
shift | 10000 | 332 array/0 linked list | 332+ |
slice | 100 | 43 array/313 linked list | 0.14 |
some | 1 | 10 array/14 linked list | 0.71 |
sort | 1 | 788 array/750 linked list | 1.05 |
sortOn | 1 | 171 array/203 linked list | 0.84 |
splice | 10000 | 20 array/40 linked list | 0.5 |
unshift | 10000 | 767 array/16 linked list | 47.94 |
It sure looks like a lot of the optimizations were successful. To be sure, let’s compare with the results from the initial implementation and compute some speedups:
Function | Initial Implementation | First Optimizatioon Pass | Speedup Factor |
---|---|---|---|
traverse | 13 | 14 | 0.93 |
elementAt/[] | 2614 | 1567 | 1.67 |
concat | 176 | 64 | 2.75 |
every | 15 | 15 | 1 |
filter | 104 | 47 | 2.21 |
forEach | 14 | 14 | 1 |
indexOf | 21 | 22 | 0.95 |
join | 27 | 27 | 1 |
lastIndexOf | 21 | 21 | 1 |
map | 12 | 4 | 3 |
pop | 5 | 5 | 1 |
push | 85 | 86 | 0.99 |
reverse | 140 | 141 | 0.99 |
shift | 1 | 0 | n/a |
slice | 908 | 313 | 2.9 |
some | 14 | 14 | 1 |
sort | 848 | 750 | 1.13 |
sortOn | 254 | 203 | 1.25 |
splice | 63 | 40 | 1.58 |
unshift | 15 | 16 | 0.94 |
Average | — | — | 1.41 |
There is clearly some statistical variance in the above numbers, which account for some inexplicable slowdowns. Variance leading to speedups should hopefully counter for this. After all, empirical testing like this is never an exact science. Speaking of inexactness, shift took 0 milliseconds due to getTimer()‘s millisecond granularity. I would normally increase the number of iterations to avoid such a scenario, but keeping the number of iterations constant across tests was key here. So there is some good speedup in the shift function, but I’m not measuring it. For the average, I have counted the shift speedup factor as simply 1 (unchanged) leading to a conservative average of 41%
Please let me know in the comments if you find any bugs or have any suggestions, especially for optimizations.