Even Faster Linked Lists
Linked lists can be much faster than AS3’s Array
and Vector
classes if you use them under the right circumstances. It’s been over a year and a half since I last visited the topic, but today it’s time to update my LinkedList
class. Read on for the freshly-optimized code and all-new performance testing and analysis!
It’s been a while since the last linked list article and much has changed with both Flash and with me. For starters, Flash Player 10.3 is now the current version as opposed to 10.0. Yes, I updated the performance results for 10.1 and 10.2, but there was no update series for 10.3 since it followed 10.2 so quickly and without promise of AS3 optimizations. The Flex SDK has also seen two major version releases in going from 3.5a to 4.5.1. More importantly, I’ve learned more about Flash optimization in that time and have therefore implemented a few changes:
- Improved performance by converting
switch
statements toif-else
chains. - Removed redundant initialization of many integers to zero
- Avoided activation objects by converting the performance testing app’s
log
function from being locally-declared to being a private field.
I also made one functional change: I consolidated sort
and sortOn
into a single sort
that takes a sort function, much like Vector.sort
. This allowed me to avoid converting the whole list to a newly-allocated Array
, calling Array.sort
/Array.sortOn
on it, and then converting back to a linked list. This was enormously expensive and involved a lot of object allocation and garbage object creation, not to mention the hidden object allocation produced by Array.sort
and Array.sortOn
. Instead, I use a merge sort (ported from Simon Tatham‘s C version) to sort the linked list in place.
Lastly, there was a bug in the previous tests where the shift
test wasn’t immediately followed by the unshift
test due to the tests being alphabetized. This meant that the tests for slice
, some
, sort
, sortOn
, and splice
were operating on a much smaller array/list than the other tests. This has been corrected.
Without further ado, here is the updated LinkedList
class:
LinkedListNode
/* The MIT License Copyright (c) 2011 Jackson Dunstan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 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
/* The MIT License Copyright (c) 2011 Jackson Dunstan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 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 * http://JacksonDunstan.com * @author Simon Tatham (sort functionality) * http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html */ 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; var j:int; // Element is in the first half, start at beginning if (index < halfLength) { while (true) { j = index - i; if (j == 0) return cur.data; else if (j == 1) return cur.next.data; else if (j == 2) return cur.next.next.data; else if (j == 3) return cur.next.next.next.data; else if (j == 4) return cur.next.next.next.next.data; else if (j == 5) return cur.next.next.next.next.next.data; else if (j == 6) return cur.next.next.next.next.next.next.data; else if (j == 7) return cur.next.next.next.next.next.next.next.data; else if (j == 8) return cur.next.next.next.next.next.next.next.next.data; else if (j == 9) return cur.next.next.next.next.next.next.next.next.next.data; else if (j == 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) { j = i - index; if (j == 0) return cur.data; else if (j == 1) return cur.prev.data; else if (j == 2) return cur.prev.prev.data; else if (j == 3) return cur.prev.prev.prev.data; else if (j == 4) return cur.prev.prev.prev.prev.data; else if (j == 5) return cur.prev.prev.prev.prev.prev.data; else if (j == 6) return cur.prev.prev.prev.prev.prev.prev.data; else if (j == 7) return cur.prev.prev.prev.prev.prev.prev.prev.data; else if (j == 8) return cur.prev.prev.prev.prev.prev.prev.prev.prev.data; else if (j == 9) return cur.prev.prev.prev.prev.prev.prev.prev.prev.prev.data; else if (j == 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; 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; 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; 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; 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; 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; 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 < 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; 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(cmp:Function): void { var p:LinkedListNode; var q:LinkedListNode; var e:LinkedListNode; var tail:LinkedListNode; var oldhead:LinkedListNode; var insize:int; var nmerges:int; var psize:int; var qsize:int; var i:int; var list:LinkedListNode = this.head; /* * Silly special case: if `list' was passed in as null, return * null immediately. */ if (!list) return; insize = 1; while (1) { p = list; oldhead = list; /* only used for circular linkage */ list = null; tail = null; nmerges = 0; /* count number of merges we do in this pass */ while (p) { nmerges++; /* there exists a merge to be done */ /* step `insize' places along from p */ q = p; psize = 0; for (i = 0; i < insize; i++) { psize++; q = q.next; if (!q) break; } /* if q hasn't fallen off end, we have two lists to merge */ qsize = insize; /* now we have two lists; merge them */ while (psize > 0 || (qsize > 0 && q)) { /* decide whether next element of merge comes from p or q */ if (psize == 0) { /* p is empty; e must come from q. */ e = q; q = q.next; qsize--; } else if (qsize == 0 || !q) { /* q is empty; e must come from p. */ e = p; p = p.next; psize--; } else if (cmp(p.data,q.data) <= 0) { /* First element of p is lower (or same); * e must come from p. */ e = p; p = p.next; psize--; } else { /* First element of q is lower; e must come from q. */ e = q; q = q.next; qsize--; } /* add the next element to the merged list */ if (tail) { tail.next = e; } else { list = e; } /* Maintain reverse pointers in a doubly linked list. */ e.prev = tail; tail = e; } /* now p has stepped `insize' places along, and q has too */ p = q; } tail.next = null; /* If we have done only one merge, we're finished. */ if (nmerges <= 1) /* allow for nmerges==0, the empty list case */ { this.head = list; return; } /* Otherwise repeat, merging lists twice the size */ insize *= 2; } this.head = list; } 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 < 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; 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; } } }
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 { private var logger:TextField = new TextField(); private function row(...cols): void { logger.appendText(cols.join(",") + "\n"); } public function LinkedListPerformance() { logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); var beforeTime:int; var afterTime:int; var arrayTime:int; var listTime:int; const VALUE:Object = {val:33}; const ALTERNATE_VALUE:Object = {val:44}; const SIZE:int = 10000; const ELEMENTATINDEX:int = SIZE / 2; const SPLICE_START:int = SIZE / 2; const SLICE_START:int = SIZE; const SLICE_END:int = SIZE; const TRAVERSES:int = 1000; const ELEMENTATS:int = 1000000; const CONCATS:int = 100; const EVERYS:int = 1000; const FILTERS:int = 100; const FOREACHS:int = 1000; const INDEXOFS:int = 1000; const JOINS:int = 10; const MAPS:int = 100; const PUSHPOPS:int = 100000; const REVERSES:int = 100; const SLICES:int = 100000; const SOMES:int = 1000; const SORTS:int = 10; const SPLICES:int = 10000; const UNSHIFTSHIFTS:int = 100000; var curNode:LinkedListNode; var i:int; var j:int; var array:Array; var list:LinkedList; array = []; list = new LinkedList(); for (i = 0; i < SIZE; ++i) { array[i] = VALUE; list.push(VALUE); } row("Function", "Array", "Linked List"); beforeTime = getTimer(); for (j = 0; j < TRAVERSES; ++j) { for (i = 0; i < SIZE; ++i) { array[i]; } } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (j = 0; j < TRAVERSES; ++j) { for (curNode = list.head; curNode; curNode = curNode.next) { curNode.data; } } listTime = getTimer() - beforeTime; row("traverse", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < ELEMENTATS; ++i) { array[ELEMENTATINDEX]; } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < ELEMENTATS; ++i) { list.elementAt(ELEMENTATINDEX); } listTime = getTimer() - beforeTime; row("elementAt/[]", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < CONCATS; ++i) { array.concat(array); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < CONCATS; ++i) { list.concat(list); } listTime = getTimer() - beforeTime; row("concat", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < EVERYS; ++i) { array.every(arrayCallbackTrue); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < EVERYS; ++i) { list.every(listCallbackTrue); } listTime = getTimer() - beforeTime; row("every", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < FILTERS; ++i) { array.filter(arrayCallbackTrue); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < FILTERS; ++i) { list.filter(listCallbackTrue); } listTime = getTimer() - beforeTime; row("filter", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < FOREACHS; ++i) { array.forEach(arrayCallbackTrue); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < FOREACHS; ++i) { list.forEach(listCallbackTrue); } listTime = getTimer() - beforeTime; row("forEach", arrayTime, listTime); array[SIZE-1] = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { array.indexOf(ALTERNATE_VALUE); } arrayTime = getTimer() - beforeTime; array[SIZE-1] = VALUE; list.tail.data = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { list.indexOf(ALTERNATE_VALUE); } listTime = getTimer() - beforeTime; row("indexOf", arrayTime, listTime); list.tail.data = VALUE; beforeTime = getTimer(); for (i = 0; i < JOINS; ++i) { array.join(); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < JOINS; ++i) { list.join(); } listTime = getTimer() - beforeTime; row("join", arrayTime, listTime); array[0] = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { array.lastIndexOf(ALTERNATE_VALUE); } arrayTime = getTimer() - beforeTime; array[0] = VALUE; list.head.data = ALTERNATE_VALUE; beforeTime = getTimer(); for (i = 0; i < INDEXOFS; ++i) { list.lastIndexOf(ALTERNATE_VALUE); } listTime = getTimer() - beforeTime; row("lastIndexOf", arrayTime, listTime); list.head.data = VALUE; beforeTime = getTimer(); for (i = 0; i < MAPS; ++i) { array.map(arrayCallbackTrue); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < MAPS; ++i) { list.map(listCallbackTrue); } listTime = getTimer() - beforeTime; row("map", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { array.pop(); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { list.pop(); } listTime = getTimer() - beforeTime; row("pop", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { array.push(VALUE); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < PUSHPOPS; ++i) { list.push(VALUE); } listTime = getTimer() - beforeTime; row("push", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < REVERSES; ++i) { array.reverse(); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < REVERSES; ++i) { list.reverse(); } listTime = getTimer() - beforeTime; row("reverse", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { array.shift(); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { list.shift(); } listTime = getTimer() - beforeTime; row("shift", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { array.unshift(VALUE); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < UNSHIFTSHIFTS; ++i) { list.unshift(VALUE); } listTime = getTimer() - beforeTime; row("unshift", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < SLICES; ++i) { array.slice(SLICE_START, SLICE_END); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < SLICES; ++i) { list.slice(SLICE_START, SLICE_END); } listTime = getTimer() - beforeTime; row("slice", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < SOMES; ++i) { array.some(arrayCallbackFalse); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < SOMES; ++i) { list.some(listCallbackFalse); } listTime = getTimer() - beforeTime; row("some", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < SORTS; ++i) { array.sort(); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < SORTS; ++i) { list.sort(cmp); } listTime = getTimer() - beforeTime; row("sort", arrayTime, listTime); beforeTime = getTimer(); for (i = 0; i < SPLICES; ++i) { array.splice(SPLICE_START, 5, VALUE, VALUE, VALUE, VALUE, VALUE); } arrayTime = getTimer() - beforeTime; beforeTime = getTimer(); for (i = 0; i < SPLICES; ++i) { list.splice(SPLICE_START, 5, VALUE, VALUE, VALUE, VALUE, VALUE); } listTime = getTimer() - beforeTime; row("splice", arrayTime, listTime); } 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; } private function cmp(a:*, b:*): int { return a.val - b.val; } } }
I ran the performance test with the following environment:
- Flex SDK (MXMLC) 4.5.1.21328, compiling in release mode (no debugging or verbose stack traces)
- Release version of Flash Player 10.3.181.34
- 2.4 Ghz Intel Core i5
- Mac OS X 10.6.8
And got these results:
Function | Array | Linked List |
---|---|---|
traverse | 68 | 37 |
elementAt/[] | 7 | 8697 |
concat | 45 | 507 |
every | 377 | 888 |
filter | 83 | 338 |
forEach | 370 | 855 |
indexOf | 103 | 151 |
join | 107 | 114 |
lastIndexOf | 105 | 155 |
map | 85 | 338 |
pop | 2 | 2 |
push | 5 | 32 |
reverse | 5 | 109 |
shift | 1241 | 2 |
unshift | 1251 | 19 |
slice | 30 | 16 |
some | 3771 | 9078 |
sort | 2490 | 1424 |
splice | 10 | 216 |
Here are the same results in graph form:
It’s much easier to see if we remove the elementAt/[]
test:
The old version’s result for sort
showed it roughly on par with Array
and sortOn
was about half as fast as Array
. In the new version, sort
is now almost twice as fast as Array.sort
! Really, the advantages are still greater as the linked list version now completely avoids object allocation (and subsequent garbage collection).
Also, for simplicity’s sake the above results do not show the old, switch
-based elementAt/[]
. However, it was clocking in at about 9350 milliseconds so, believe it or not, the new if-else
chain version is actually about 8% faster. Considering how terrible the linked list performance is anyhow, this isn’t a big deal.
So how do linked lists fare with a new Flash Player, a new Flex SDK, a new sort function, and some miscellaneous other improvements? Well, as you can see from the above data and graphs, the results are quite mixed. Here is a table that simplifies the advantages on a function-by-function basis:
Function | Best Data Structure | Speedup |
---|---|---|
traverse | LinkedList | 2x |
elementAt/[] | Array | 1000x |
concat | Array | 10x |
every | Array | 2x |
filter | Array | 4x |
forEach | Array | 2x |
indexOf | Array | 50% |
join | –tie– | n/a |
lastIndexOf | Array | 50% |
map | Array | 4x |
pop | –tie– | n/a |
push | Array | 5x |
reverse | Array | 20x |
shift | LinkedList | 600x |
unshift | LinkedList | 500x |
slice | LinkedList | 2x |
some | Array | 3x |
sort | LinkedList | 2x |
splice | Array | 20x |
In every case except join
and pop
the winner is by a large margin. It’s therefore important to keep in mind how you will be using the data structure when you decide which to go with. Doing a lot of random access by index or searching for values? Array
wins by a landslide. Doing a lot of sorting or traversing? LinkedList
destroys Array
.
Download the LinkedList
and LinkedListNode
classes as a ZIP file.
Spot a bug? Have an optimization to suggest? Post a comment!
#1 by Eugene on July 11th, 2011 ·
Hey
i dont know if u have seen me writing about it:
https://gist.github.com/1034474
it is very simple but fast linked list approach with fastest sort method included.
i don’t know how it compares to yours but may be u will be interested to take a look
#2 by jackson on July 11th, 2011 ·
I hadn’t seen, but I took a look and it looks like an interesting approach. A free list of nodes for a linked list is a bit mind-bending, but perhaps a good idea for integration into the class.
#3 by Roland Zwaga on July 11th, 2011 ·
Hey there,
great introduction article to the world of linked lists!
Just wondering, are you familiar with the as3commons-collections library?
http://www.as3commons.org/as3-commons-collections/index.html
It offers linked lists and many other collection related classes, you might find it interesting.
cheers,
Roland
#4 by jackson on July 11th, 2011 ·
Hey Roland, glad to hear you enjoyed the article. I actually have heard of AS3-Commons before but hadn’t looked at their linked list implementation before. I just took a look at it now and see that it is elegantly designed and reasonably full-featured. However, it differs from my own implementation in two ways. First, and most importantly, it does not seem to have a focus on high performance. For example, the
count
method walks the entire list rather than keeping a cached integer variable around for quick checking. Second, it does not attempt to mimic the API ofArray
orVector
, which mine (more or less) does. As such, the API will be unfamiliar to AS3 programmers and some functionality will be either different or missing. I’m not claiming that my version is outright superior to theirs, only that we have differences. Perhaps I’ll do a follow-up test comparing the two implementations…Thanks for the link!
#5 by Jens on July 26th, 2011 ·
Hello Jackson, thanks for your comment about the AS3Commons LinkedList. I know that your foremost goal is high performance, and I was wondering why do you think that the AS3Commons list does not focus on performance, too. In fact, this class is highly performance optimized and may hardly be improved using AS3. Nevertheless, performance is not the sole goal of AS3Commons Collections. Reusability and a consistent API are other principles that the particular implementations follow. For example, the linked list there is reused to back a linked map and a linked set. Considering the different aspects, the resulting code must be a tradeoff between performance, usability and maintainability.
I was indeed curious to see both linked lists performing. I did some tests with them and additionally with a very raw linked list implementation.
Items: 1.000.000 ints
test: AS3Commons / JD / Raw
addFirst: 241 ms / 285 ms / 244 ms
addLast: 249 ms / 301 ms / 248 ms
removeFirst: 39 ms / 26 ms / 25 ms
removeLast: 37 ms / 25 ms / 25 ms
The test says that the runtime of operations of each class is nearly equal. Adding to JD is slightly slower. Removing then slightly faster. Inspecting the code, I found that removing the last item from the JD list does not clear the head and tail references which may cause errors. Adding that code will slightly reduce the execution speed of pop and shift.
#6 by Jens on July 26th, 2011 ·
Forgot the tests:
JD LinkedList test: http://code.google.com/p/sibirjak/source/browse/trunk/asperform/com/sibirjak/asperform/collectiontests/jacksondunstan/JDLinkedListTest.as
AS3Commons LinkedList test: http://code.google.com/p/sibirjak/source/browse/trunk/asperform/com/sibirjak/asperform/collectiontests/as3commons/AS3CommonsLinkedListTest.as
Raw LinkedList test: http://code.google.com/p/sibirjak/source/browse/trunk/asperform/com/sibirjak/asperform/collectiontests/examples/linkedlist/LinkedListRawTest.as
#7 by jackson on July 26th, 2011 ·
Forgive me, but I’m suspicious of large testing frameworks. :) I therefore made the following quick test:
I then made the modifications you suggested to my own version, downloaded the development version (1.3.1) of AS3 Collections, and ran it on Windows 7 with the release 10.3 player. Here’s what I got:
So we seem to have different test results since my simple test shows the AS3 Collections implementation to be slower in all cases.
#8 by Jens on July 27th, 2011 ·
Interestingly the runtime highly depends on the execution order of the test methods.
There will probably be no exact result. However, swapping the methods looks then like this. No difference in list population. JD wins on removals.
#9 by jackson on July 27th, 2011 ·
Order 2:
Order 3:
I too am seeing the
push
times reverse in that case, which is quite strange. The other three tests still show an advantage to my version though. I notice that your results have a third column of data in them and you didn’t post your test source. Are you perhaps using the AS3 Collections test framework while I am testing more directly (like in my articles)?#10 by Jens on July 27th, 2011 ·
haha … no, i just wanted to see the number of items processed. It’s your code.
#11 by skyboy on July 12th, 2011 ·
While looking over your code, I noticed that splice doesn’t insert anything from the values array, as Array does.
You could also reuse the nodes you’re removing by simply saving the start node and tail node to variables, then assigning them to the head/tail of the return list and decoupling those two nodes; less allocations and deallocations, plus it can run faster without the unlinking+if+new.
Something like:
You might also optimize single element splices, since those would be most common and are extremely fast.
#12 by skyboy on July 12th, 2011 ·
LT = <
EQ = ==
GTEQ = >=
#13 by jackson on July 12th, 2011 ·
Excellent suggestions. I feel a follow-up article coming on. :)
#14 by skyboy on July 13th, 2011 ·
I just modified the test slightly, and threw in my standing optimization of the previous LinkedList post, merging the sort method and adapting it to sortOn as well and got this:
For a random set of data (
{val:Math.random() * int.MAX_VALUE}
).Array was passed the same comparator function the other two are, for sortOn LV/Array had their methods called and LinkedList had sort called with the comparator function.
Your some and other similar methods could be vastly improved by implementing a scheme similar to what i suggested for splice:
However, elementAt can’t be improved much further. Best I got was 2% faster by simply doing this:
while (index--) cur = cur.next;
. I can see no improvements being made on this, no matter how many elements are added, or what variation of duff’s device is being used.The choke point for it is in the cache misses made while accessing each Node. The fragmentation of LinkedLists in memory means processors can’t prefetch Nodes in a way that remotely benefits anything. Reducing code complexity will make it easier to read, and remove more overhead than anything else that can be done at the level the VM allows us
#15 by jackson on July 13th, 2011 ·
I’d forgotten about LinkedVector! The followup article will definitely have to take some of those optimizations into account. The
some
optimization to avoidcall
is a particularly easy optimization, it seems.Which sort are you using in the above numbers? The version of LinkedVector on GitHub still shows a
toArray
/fromArray
approach.#16 by skyboy on July 13th, 2011 ·
I used your AS3 port of the linkedlist mergesort; I further modified it last night and am now getting performance out of it I’m not certain I should believe is working correctly:
I have updated the github with the changes now; There’s a potential bug in your sorting as well: the tail reference isn’t updated afterwards.
#17 by jackson on July 13th, 2011 ·
Thanks again. The next revision will be much better because of all of this. :)
#18 by skyboy on July 14th, 2011 ·
The sort was incorrect, and having corrected it the performance is now between 4x and 10x slower than Array::sortOn, depending on sort method chosen (case insensitive comparator sort being slowest). I also added a test for toString and got a rather large shock:
This was run in the stand-alone 10.3 release player, but somehow yours (which is barely different from join) manages to consistently have performance 10x higher than join. It may be related to the constant string expression.
I’ve managed to get both join and toString to very closely match (and at times, beat) Array’s methods by using a ByteArray with length predetermined to 25 characters / element in an effort to avoid costly concats between strings. With elements consisting of essentially ints, it does rather well. Strings should perform better and Arrays / other large structures worse.
Depending on how tests play out on your machine (and mine in ~10 hours, after a restart), you may want to look into a similar method.
My splice implementation, however, performs very poorly. 10x worse than yours, at best. I think I’ll just rewrite it entirely.
#19 by jackson on July 14th, 2011 ·
The
toString
performance you’re seeing is quite odd. Can using a string literal really be that much more expensive? I guess that’s one more thing to test (and optimize)…What about the
sort
was incorrect?#20 by skyboy on July 14th, 2011 ·
I incorrectly optimized out the null check on q causing null values to have properties accessed, and the outermost loop terminates after just one pass (hiding the first problem) because I forgot to change .
I missed any problems initially because the push/pop and shift/unshift tests order were removing the random elements and replacing them with constant values, and the sorting order check I placed after didn’t see anything wrong.
#21 by Damian on March 7th, 2012 ·
+1 for the tail reference not being updated after the sort. you need to add
this.tail = tail
right aftertail.next = null
It (the sort) also seems to be consistently slower than Array.sort() or Vector.sort() (passing a comparison function). I’ve not done any extensive speed testing on it though. But then, it’s not something I’m going to lose sleep over :)
#22 by skyboy on July 17th, 2011 ·
I only just realized this, but you should reverse the order of push/pop and unshift/shift, the lengths are being greatly inflated after these operations causing the times to be inaccurate in relation to the other tests. Shifting/popping values that don’t exist could also be impacting relative performance as well, making each look better or worse, depending on how soon its handled.
#23 by jackson on July 17th, 2011 ·
Another good catch. I may even pair them into a “push/pop” test if
Array
has worse performance at larger lengths.Thanks for adding so much to the (inevitable) follow-up article!
#24 by skyboy on July 17th, 2011 ·
I don’t think Array will perform worse than LinkedList at larger lengths, since Array does fewer allocations.
Though, if you’ve not modified splice already, you will have to after reversing the order of the two sets; the last element is touched because elements aren’t added back, and it immediately fails by trying to access
tail.next.next
. My last attempt at optimizing splice and rewriting most of it only resulted in a 20% speed gain from previous, putting it at 8x worse than your current implementation. Though more stable.#25 by Barliesque on July 21st, 2011 ·
I wonder if you’ve had a look at this library…
http://code.google.com/p/polygonal/wiki/DataStructures
…which was written in C++ and compiled with Alchemy for use in AS3.
I have yet to make use of it myself, but would be very interested to see how the Linked List implementation there measures up in performance.
#26 by Barliesque on July 21st, 2011 ·
…whoops. No, it was written in HaXe.
#27 by Barliesque on July 22nd, 2011 ·
…whoops. No, it was written in HaXe for AS3 output.
#28 by Jens on July 26th, 2011 ·
My array sort test results:
Items: 1.000.000 ints
array.sort() – 1156 ms
array.sort(Array.NUMERIC) – 26 ms
array.sort(cmp) – 146 ms
JD LinkedList – 179 ms
#29 by jackson on July 26th, 2011 ·
Interesting, especially as none of those is even close to the 2x I got in the article. What test environment did you run on?
#30 by Jens on July 26th, 2011 ·
Array test: http://code.google.com/p/sibirjak/source/browse/trunk/asperform/com/sibirjak/asperform/collectiontests/flash/ArrayTest.as
#31 by jackson on July 26th, 2011 ·
That’s good to see too, but I was meaning in terms of Flash Player version/type, OS, CPU, etc. :)
#32 by Jens on July 26th, 2011 ·
Firefox Plugin 10,3,181,34 no debug
Windows 7, 2,6 GHz Intel Core Duo
Flex 4.1.0something
#33 by jackson on July 26th, 2011 ·
Ah, well I hadn’t run the test on Windows yet so I just did and got:
So I’m still seeing roughly 2x faster performance with my LinkedList class, even on Windows.
As I said above with the addFirst/addLast/removeFirst/removeLast test, I’m suspicious of any large testing framework. Knowing Flash’s various performance quirks, I prefer to keep things dead simple so I can control exactly what’s going on: down to crazy things like variable ordering. Given that we’re both testing on basically the same environment, I suspect the testing framework itself.
#34 by Jens on July 26th, 2011 ·
… went already home …
I found that my sort test uses randomized data.
The array.sort() method incorporates the quick sort algorithm – when i remember right. And this method is faster on nearly sorted items than the merge sort. This could be a reason. I will run the test again tomorrow with ordered data like in your test.
#35 by Jens on July 27th, 2011 ·
Took me a while to clarify the problem with the differing sort result times.
Your array and list are populated with objects. Each object hosts the same val=33.
While you call list.sort(cmp) with a comparator function, you do not with array.sort(). Don’t know what the array here does, but let us switch to numeric values instead of objects, and the runtime of array.sort() increases to a factor 7-10 of list.sort(cmp).
For a reliable comparision, we should actually consider more facts:
– adding values that have a natural order
– sorting already sorted VS randomized data
– type of data to be sorted (complex, string, numeric)
– array sort method arguments
#36 by jackson on July 27th, 2011 ·
Thanks for the suggestions. The article in the linked list series will surely incorporate them. :)
#37 by dimuMurray on August 23rd, 2011 ·
I’ve been reading and re-reading your linked list series for months now and have been quietly hacking away at my own implementation. Figured I might as well throw it out there for peer review.
So far only two possible improvements came to mind when I started to tackle this thing. The first was to eliminate the need to instantiate node objects by using two dictionaries to maintain the links instead. This breaks from the traditional implementation (i’m not cheating am I?) and will probably present a few problems down the road when I start implementing
Array
methods but I am hoping that this approach will bear good fruit.Second I’m hoping to optimize the
elementAt()
method using Skip Lists or some variation thereof.Haven’t gotten the skip list stuff integrated as yet but so far I’ve manage to cobble together the following :
It will probably be a while before I write the methods to match those of the
Array
class but it’s a start. So is this a good approach or have I gone too far off the beaten path?#38 by jackson on August 23rd, 2011 ·
What an intriguing idea! The
LinkedListNode
class has always bothered me, too. I’m really looking forward to when it’s functionally-compatible with my version so I (or you) can test them against each other. Feel free to grab as much of my version as you need as long as you’re OK with the MIT license (very permissive). Here are some random tips of the top of my head:return
afterthrow
contains
to use thein
operatorsize
is redundantly assigned0
(since that’s the default)remove
shouldn’t callcontains
) to save (expensive) function call timeLooking forward to it. :)
#39 by dimuMurray on August 23rd, 2011 ·
Thanks for the pointers. I’ll be sure to implement them wherever I can.
Looks like my code got mangled when I pasted it, all the conditionals are a mess.
#40 by skyboy on August 24th, 2011 ·
Your implementation is interestingly similar to DenseMap (incomplete). I decided to go with Vectors for the prev/next indexes, though, moving it closer to a combination of Array and LinkedList while yours is mostly a pure LinkedList implementation.
You may also get some ideas for how to implement Array’s interface for your class by looking at my code.
#41 by seaders on March 12th, 2012 ·
Hey Jackson, I’m getting some really funky behaviour with this code, when sorting, here’s the implementation,
but at the the loop at the end, if you set a breakpoint there, you will see that both the head and the tail of the linkedlist are pointing to the same GenObject and their prev is set to null and next is set to the second in the list. Not looked through the source and swapped to LinkedVector instead as this behaviour is obviously not correct, but I did want to let you know.
#42 by seaders on March 12th, 2012 ·
That’s copied in very strangely. The compFunc is
a.score b.score return -1
a.id < b.id return 1
else return 0
#43 by jackson on March 12th, 2012 ·
Sorry for the HTML mangling. My comments system thinks that the
Vector
syntax is HTML and tends to mess up posts.As for the weird behavior, it may be due to some bugs in the
LinkedList
class that have been pointed out above in the comments. Some day I will update this article to fix the issues and perhaps apply yet-another round of optimizations. :)#44 by Larry on April 30th, 2012 ·
Interesting.
It is difficult to interpret performance results at a single length. Individual performance plots of each method at various data lengths would be very interesting (i.e. list length vs. time plot for the sort function).
Here you would see big O for each function.
#45 by jackson on April 30th, 2012 ·
I agree and I’ll try to get a wider range of results in the next article in this series. In the meantime, feel free to grab the code and change
SIZE
.#46 by zhang on June 4th, 2013 ·
the elementAt’s method has a error (TypeError: Error #1009)
the local variable cur is null