Fast AS3 MultiMap
Sometimes you need to map a key to many values, but AS3 has no built-in data structure for this purpose. Dictionary
and Object
are suitable one-to-one maps, but there’s been no one-to-many support until now. Read on for my own one-to-many class—MultiMap
—as well as performance testing and analysis.
Before writing your own general-purpose class, it’s a good idea to take a few minutes and do some searching online to see if anyone else has already implemented it. I did just this and found that the Vegas Framework has a MultiValueMap
and HashMap
that can, together, form a suitable multi-map. I also found Sascha Balkau’s MultiMap class.
Clearly, I’ve written my own MultiMap
class even though these alternatives exist. Why? Well, the Vegas Framework entails pulling in a SWC with at least 957 classes and interfaces. Some of them will surely drop out when compiling, but there are at least a half dozen classes that will be compiled in just for multi-map support. In the case of Sascha Balkau’s class, there are only two classes to compile in. However, it seems as though he’s implemented his own hash map despite the existence of Dictionary
. In short, I had serious questions about both libraries’ performance, size, and license (MPL).
Still, I looked around at various multi-map APIs and tried to glean ideas from them for my own class. I decided from the start to use a Dictionary
for the main mapping and a secondary Dictionary
to store the mapped values. So the main Dictionary
would have a key “fruit” that would map to a Dictionary
whose keys would be “apple”, “pear”, and “banana”. This arrangement means that insertion, checking, and removal is all very cheap and the design’s simplicity keeps the implementation to a single 200-line class. Finally, I licensed it under the extremely-permissive MIT license.
Here are some quick usage examples:
var map:MultiMap = new MultiMap(); // insert one value at a time map.insertVal("fruit", "apple"); map.insertVal("fruit", "pear"); map.insertVal("fruit", "banana"); // insert many values at a time map.insertVals("continent", "europe", "asia", "africa"); // insert a whole object at once map.insertProperties({first:"Jackson", last:"Dunstan", profession:"programmer"}); // visualize the map trace(map.toMapString()); // fruit // -> apple // -> pear // -> banana // continent // -> europe // -> asia // -> africa // first // -> Jackson // last // -> Dunstan // profession // -> programmer // count the keys map.countKeys(); // 5 // count the values of one key map.countVals("continents"); // 3 // count the values of all keys map.countAllVals(); // 9 // copy the map map2 = map.clone(); // check if a key exists map.hasKey("fruit"); // check if a value exists in any key map.hasVal("asia"); // check if a key-value pair exists map.hasPair("first", "Jackson"); // remove a key and all its values map.removeKey("fruit"); // remove just one value from a key map.removeVal("asia"); // remove a value from one key map.removePair("continent", "africa"); // remove all keys and values map.removeAll();
Now for the full source code with more in-depth documentation:
/* 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 { import flash.utils.Dictionary; /** * A collection that maps keys to one or more values * @author Jackson Dunstan, http://JacksonDunstan.com */ public class MultiMap { /** If the keys of the map are weakly-referenced */ private var __weakKeys:Boolean; /** Mapping of keys to Dictionaries of values */ private var __keys:Dictionary; /** * Make the map * @param weakKeys If the keys of the map are weakly-referenced. * Defaults to false. */ public function MultiMap(weakKeys:Boolean=false) { __keys = new Dictionary(weakKeys); __weakKeys = weakKeys; } /** * If the keys of the map are weakly-referenced */ public function get weakKeys(): Boolean { return __weakKeys; } /** * Mapping of keys to Dictionaries of values. Beware changing this. */ public function get keys(): Dictionary { return __keys; } /** * If the map has no keys (or values) */ public function get isEmpty(): Boolean { for (var key:* in __keys) { return false; } return true; } /** * Map a key to a value. Overwrites any previous mapping of the given * key and value. * @param key Key to map to the given value * @param val Value to map to the given key */ public function insertVal(key:*, val:*): void { var vals:Dictionary = __keys[key]; if (!vals) { vals = __keys[key] = new Dictionary(); } vals[val] = true; } /** * Map a key to many values. Overwrites any previous mapping of the * given and values. * @param key Key to map to the given values * @param vals Values to map to the given key */ public function insertVals(key:*, ...vals): void { var valsDict:Dictionary = __keys[key]; if (!valsDict) { valsDict = __keys[key] = new Dictionary(); } for each (var val:* in vals) { valsDict[val] = true; } } /** * Map the keys of the given object to their values. For example, * {first:"Jackson", last:"Dunstan"} maps "first" to "Jackson" and * "last" to "Dunstan". Overwrites any previous mapping of the given * object's keys and values. * @param obj Object whose properties should be mapped */ public function insertProperties(obj:*): void { for (var key:* in obj) { var vals:Dictionary = __keys[key]; if (!vals) { vals = __keys[key] = new Dictionary(); } vals[obj[key]] = true; } } /** * Get the values mapped to a given key. Beware changing this. * @param key Key to get values for * @return The values mapped to the given key or null if no values are * mapped to the given key. */ public function getVals(key:*): Dictionary { return __keys[key]; } /** * Check if a key has had values mapped to it * @param key Key to check * @return If the given key has had values mapped to it */ public function hasKey(key:*): Boolean { return key in __keys; } /** * Check if any key has been mapped to a given value * @param val Value to check * @return If any key has been mapped to a given value */ public function hasVal(val:*): Boolean { var keys:Dictionary = __keys; for (var key:* in keys) { if (val in keys[key]) { return true; } } return false; } /** * Check if the given key has been mapped to the given value * @param key Key to check * @param val Value to check * @return If the given key has been mapped to the given value */ public function hasPair(key:*, val:*): Boolean { var vals:Dictionary = __keys[key]; if (!vals) { return false; } return val in vals; } /** * Unmap the given key and all values mapped to it * @param key Key to unmap */ public function removeKey(key:*): void { delete __keys[key]; } /** * Unmap the given value from all keys * @param val Value to unmap */ public function removeVal(val:*): void { var keys:Dictionary = __keys; for (var key:* in keys) { var vals:Dictionary = keys[key]; delete vals[val]; // If the key is no longer mapped to any vals, remove it var empty:Boolean = true; for (var value:* in vals) { empty = false; } if (empty) { delete keys[key]; } } } /** * Unmap the given key-value pair * @param key Key to unmap * @param val Value to unmap */ public function removePair(key:*, val:*): void { var vals:Dictionary = __keys[key]; if (vals) { delete vals[val]; // If the key is no longer mapped to any vals, remove it var empty:Boolean = true; for (var value:* in vals) { empty = false; } if (empty) { delete keys[key]; } } } /** * Unmap all keys and values * @param key Key to unmap * @param val Value to unmap */ public function removeAll(): void { var keys:Dictionary = __keys; for (var key:* in keys) { delete keys[key]; } } /** * Count the number of keys in the map * @return The number of keys in the map */ public function countKeys(): uint { var count:uint; for (var key:* in __keys) { count++; } return count; } /** * Count the number of values in the map for the given key * @param key Key whose values should be counted * @return The number of keys in the map for the given key or zero if * the given key is not mapped */ public function countVals(key:*): uint { var count:uint; for (var key:* in __keys[key]) { count++; } return count; } /** * Count the number of values in the map for all keys * @return The number of keys in the map for all keys */ public function countAllVals(): uint { var count:uint; for (var key:* in __keys) { var vals:Dictionary = keys[key]; for (var val:* in vals) { count++; } } return count; } /** * Make a new map that has the same mappings and key strength * (i.e. weak keys, strong keys) as this map * @return A new map that has the same mappings and key strength as * this map */ public function clone(): MultiMap { var ret:MultiMap = new MultiMap(__weakKeys); var keys:Dictionary = __keys; for (var key:* in keys) { var vals:Dictionary = keys[key]; for (var val:* in vals) { ret.insertVal(key, val); } } return ret; } /** * Get a new String that shows the mappings * @return A new String that shows the mappings */ public function toMapString(): String { var ret:String = ""; var keys:Dictionary = __keys; for (var key:* in keys) { ret += key + "\n"; var vals:Dictionary = keys[key]; for (var val:* in vals) { ret += "\t-> " + val + "\n"; } } return ret; } } }
To test the performance of this class against the two other multi-map implementations I found, I made the following performance test app that checks how fast each version can perform the core operations of a multi-map: add mappings, check if mappings exist, and remove mappings.
package { import flash.display.*; import flash.utils.*; import flash.text.*; import system.data.maps.MultiValueMap; import system.data.maps.HashMap; public class MultiMapTest extends Sprite { private var __logger:TextField = new TextField(); private function row(...cols): void { __logger.appendText(cols.join(",") + "\n"); } public function MultiMapTest() { __logger.autoSize = TextFieldAutoSize.LEFT; addChild(__logger); var beforeTime:int; var afterTime:int; var jdTime:int; var sbTime:int; var vTime:int; var i:int; var REPS:int = 20000; var mapJD:MultiMap = new MultiMap(); var mapSB:SBMultiMap = new SBMultiMap(100, SBMultiMap.hashString); var mapV:MultiValueMap = new MultiValueMap(null, HashMap); row("Function", "JD", "SB", "V"); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapJD.insertVal("Key", "Value"); } afterTime = getTimer(); jdTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapSB.put("Key", "Value"); } afterTime = getTimer(); sbTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapV.put("Key", "Value"); } afterTime = getTimer(); vTime = afterTime - beforeTime; row("add", jdTime, sbTime, vTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapJD.hasKey("Key"); } afterTime = getTimer(); jdTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapSB.containsKey("Key"); } afterTime = getTimer(); sbTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapV.containsKey("Key"); } afterTime = getTimer(); vTime = afterTime - beforeTime; row("add", jdTime, sbTime, vTime); beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapJD.removePair("Key", "Value"); } afterTime = getTimer(); jdTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapSB.remove("Key"); } afterTime = getTimer(); sbTime = afterTime - beforeTime; beforeTime = getTimer(); for (i = 0; i < REPS; ++i) { mapV.removeByKey("Key", "Value"); } afterTime = getTimer(); vTime = afterTime - beforeTime; row("add", jdTime, sbTime, vTime); } } }
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.35
- 2.4 Ghz Intel Core i5
- Mac OS X 10.6.8
And got these results:
Function | JacksonDunstan.com | Sascha Balkau | Vegas Framework |
---|---|---|---|
add | 4 | 4597 | 9 |
contains | 1 | 8968 | 3 |
remove | 3 | 9632 | 71 |
For a better comparison between my version and the Vegas Framework version, let’s chop out Sascha Balkau’s version from the chart.
Clearly, Sascha Balkau’s MultiMap
is a ton slower than the others on all three tests. This slowdown is usually in the range of about 1000-3000x, which makes it only suitable to applications that can afford to burn a lot of performance. As for the Vegas Framework version, it’s 2x-20x slower than my own version.
So, if you want a Flash 9+ AS3 multi-map implementation with a permissive open-source license (MIT), a single-class implementation for a la carte usage, and the best performance of them all by a good margin, choose my MultiMap
class! :)
Spot a bug? Have a suggestion? Question on usage? Post a comment!
#1 by as3isolib on July 25th, 2011 ·
I have use for this in the as3isolib.v2. Hopefully I can find some time here soon to do some testing. My question is this: How reliably can you count on weak Dictionaries to release vs. an explicit call to delete a key(s). Not that your MultiMap has a huge penalty for calling remove(), but being called several hundreds or thousands of times during a render pass, is it better to use an explicit call or allow the GC to collect?
I thought you had written an article on this very question (explicit delete vs. weak GC collection) but I can’t seem to find it.
#2 by jackson on July 25th, 2011 ·
Glad to hear that this will be useful to you. :)
As for weak references, the only article I’ve ever written explicitly about them was my WeakReference class a couple of years ago. The garbage key will definitely be collected at some point, but the Flash Player doesn’t make any guarantees about when this will be. It’s probably best if you call
removeX
if you need the item to not be in the map (or any weak-keyDictionary
) right now and waiting for a GC pass would cause you problems. If you’re simply uninterested in the key and only care about it getting freed sometime in the next few frames, you can probably leave it up to the GC and save some time by not callingremoveX
.#3 by skyboy on July 25th, 2011 ·
Though, if 512 objects are allocated during the render pass, the incremental GC will kick in and sweep regardless of if its needed or not; and every time an additional 512 are allocated. It causes my JSON stress / performance tests grief.
#4 by as3isolib on July 25th, 2011 ·
Grief in what way? Are you seeing a huge drop in performance when leaving to the GC? I’d def be using more than 512 objects on a seemingly simple scene due to the object rendering i am utilizing.
Thanks for the comments!
#5 by skyboy on July 26th, 2011 ·
It’s not a huge drop, usually, but in a recent stress test (parsing / encoding 6 MB) the performance continues to drop as the test goes on (with the second and subsequent tests taking at least a full 8% longer due to some non-incremental garbage collection) due to the number of objects created and destroyed, numbering in the millions.
Disabling the incremental collection via mm.cfg dramatically improves performance for some things, string concatenation seems to be the most pronounced, at a whopping 50% or more.
That’s a small except showing the impact on various methods. The two haXe results are of the time my code takes, and the time my code takes plus the time that’s spent in ByteArray::readObject; My AS3 method allocates the majority of objects at the start, so I manage to scrape past with a negligible 3% performance impact; the haXe method has the same impact, but a much greater relative difference of 10%. Meanwhile the Vegas method has the most significant impact for parsing, 15%. Two mysteriously operate faster with the incremental GC on, as3corelib and the ekameleon package.
The encoding methods show much more pronounced impacts, excepting my method and the as3corelib method.
Both tests were done with the release FP11 ActiveX; the IE8 process set to “realtime” in the process manager to remove the impact of other programs during the test.
#6 by Philippe on July 25th, 2011 ·
Nice class, no junk, code is straightforward.
If I remember correctly I had garbage collection issues related to doing: dict[val] = val
#7 by jackson on July 25th, 2011 ·
I just made a little test app and found that you are correct: the GC is not collecting any of these objects, even though they have weak keys. I’ve updated the article and the class so that the value of the second-level
Dictionary
is simplytrue
. The value wasn’t being used anyway, so this is really just a placeholder.Thanks for pointing this out!
#8 by Philippe on July 25th, 2011 ·
That’s what I ended doing too – thanks for confirming my memory.
Otherwise I’d be able to discuss endlessly a class like that and I think the double insertVal/insertVals is useless (should just be one method). Eh, I’ll probably copy and modify your code at will ;)
#9 by jackson on July 25th, 2011 ·
I did a quick performance test of
insertVal
andinsertVals
and find thatinsertVal
is about 3x faster thaninsertVals
:You’re, of course, free to do as you wish though. :)
#10 by divillysausages on July 26th, 2011 ·
I’ve noticed in a lot of your classes (for example, this one and the previous LinkedList), you reuse a lot of code rather than have common functions. For example, insertProperties() could call insertVal() repeatedly. Why do you do this? Is it to avoid the tiny speed penalty for calling functions? It seems to go against the whole DRY principle
#11 by jackson on July 26th, 2011 ·
It definitely goes against the DRY principle, but I duplicate for the sake of speed. For example, I made an
insertValsReuse
that reusesinsertVal
like so:I then tested the performance when there are 10 vals passed in and found
insertValsReuse
to be 12.7% slower thaninsertVals
. I increased the number of vals to 20 (easily possible with the suggested change toinsertProperties
) and foundinsertValsReuse
to be 21.6% slower.It’s definitely a judgement call, but I prefer to duplicate a few lines of code here and there to keep my code running fast.
#12 by lyua on July 26th, 2011 ·
Hi Jackson, I read one of you previous article and have found in discussion the similar tools from Jens Struwe. Have you compared you code with his Map (http://sibirjak.com/osflash/blog/collections-framework-performance-comparision/)? Just interesting to see differences. btw, thanks for you code, it is really light ;). Looks like it is a time to collect you classes (Map, LinkedList) in one package! wdyt?
#13 by jackson on July 26th, 2011 ·
Glad to hear you like the class. I may package it up some day when I have more classes to put in such a package. As for the comparison, it’s good to see that other people are doing such comparisons. However, there was no test with multi-maps because none of the tested libraries seem to have that functionality. If they do add a multi-map, I may do a test of my class against them.
Thanks for the link. :)
#14 by lyua on July 26th, 2011 ·
for count*() methods. Do we have chance to change them just to properties and inc/dec these properties in related add/remove methods rather than calculate them each time when client’s code calls it?
#15 by lyua on July 26th, 2011 ·
looks like below. seems performance of all count*() and some remove*() methods should increase. Method toMapStringWithCount() just added for debugging.
#16 by jackson on July 26th, 2011 ·
I definitely considered this when writing the class. However, I didn’t do it for a few reasons:
Dictionary
andObject
(one-to-one maps) don’t provide a countThe code you posted has a couple of issues that both cause an inaccurately-high count. First, when an insert function overwrites a value, you still increment the count. Second, if the GC collects based on a weak key, your count is too high. The first issue can be avoided by checking for duplicates, but that would entail extra code and time to do the checking, which may result in expensive comparisons like
String==String
which checks each character. The second problem can only be avoided by doing just what my count functions do: verifying the count by looping over theDictionary
objects.So for accuracy’s sake as well as some other more minor concerns, I chose to keep the counting like
Dictionary
. It’s a shame that it’s slow to get a count, but hopefully that case doesn’t come up much. You can always extendMultiMap
(e.g. aCountedMultiMap
) for special cases where you know you’re not going to use weak keys or duplicates, but in the general case implementation I’ve provided it’s probably best to keep things accurate for everyone.#17 by lyua on July 27th, 2011 ·
I am not sure that checking for duplicates brings a lot of code; actually, it is already in the code… like
if ( !valsDict[val] )
and the similar in the all others insert*() methods. I checked with calls likeand it counts correctly, i.e ‘africa’ was not counted twice.
May be I missed something, will be good if you could point these cases.
What I found, that
map.insertProperties({oneMore: "+1", oneMore:"+2"});
populate to themap
“+1” value only. Other hand, if we runmap.insertProperties({oneMore:"+1"}); map.insertProperties({oneMore:"+2"});
it will be two values added for ‘oneMore’ key, as expected.As for weak references…it is interesting, could you provide small example where weak refs are using and break a ‘counts’?
#18 by jackson on July 27th, 2011 ·
You’re correct on the first point: I missed that you had already added the checks for duplicate keys and values. Sorry for the confusion. However, these checks do add code to the original version and will cost some performance. If you decide to keep them in, let me at least recommend that you use the
in
operator. As you can see in this article, in is the fastest version forDictionary
that won’t inadvertently trigger an expensive and erroneous operation like!String
. If that were to happen, the contents of theString
would be inspected when all you really care about is if the key exists. Imagine someone mappedfalse
to something and you checked!false
…As for the multiple inserts puzzle, in the first case you’ve created an object with two of the same key so Flash Player decided to silently drop your second value. This should really be a compiler warning at least but, annoyingly, it compiles just fine.
And here’s an example of garbage collection throwing off the key count:
When the
BitmapData
is collected, your count isn’t updated, so you should see this:I’m pretty sure the only way to cache the count would be to drop support for weak keys because they mean that you have no way to know when a key-value pair has been removed from your
Dictionary
and therefore no way to update your count without afor-in
loop.#19 by Jens on July 27th, 2011 ·
Make a switch: If your map is weak, count items in a loop and else count on insertions and removals. :D
#20 by lyua on July 28th, 2011 ·
good idea ;) actually, better to have additional class MultiMapWeak(), at least it will not require switch in count methods.
#21 by lyua on July 28th, 2011 ·
Thanks for example! As for me, I don’t like weak references so much, and for map it is better, for my opinion, to use direct destroy calls — once you call ‘new’, you should call ‘remove/destroy’ :)
As for ‘!’ or ‘in’ using, I see you code also use ‘!’ for checking a new keys. Please try following (of course, this is a looks like an academic-fake example):
Seems it provides wrong result, following to
toMapString()
:false
–> false
and seems to be expected as in comments:
the same weird is happened with
null
:)Of course, during development it is good know to avoid these cases, but if you will use objects for map’s keys ‘null’ may appear run-time. Thoughts type validation for insert*() methods needed here, WDYT?
#22 by jackson on July 29th, 2011 ·
The key difference is that I’m using the
!
operator only onDictionary
types, not the keys or values stored in the map, which could be any type. As such, I’ll never have the case where I inadvertently do!false
or!null
and get true when the value actually does exist.As for the issues you’re seeing, the
false
/null
key is being overwritten by theString
version. Here’s an example that shows it outside of theMultiMap
context for simplicity’s sake:The examples you show therefore overwrite the
Boolean
false with theString
“false” two times (one key, one value) and you get the result of a single remaining key and value. That said, I too find this behavior to be totally unexpected!#23 by lyua on July 29th, 2011 ·
may be it is extra, but idea of possible fix below is to define key as a complex/aggregated key, like pair
[key type][key value]
; and the same for values. See below two changed methods insertVal(key:*, val:*) and for debugging toMapStringWithCount().I tested them with this cases:
It traces some relevant, looks to me results:
false~boolean~(2)
— false~boolean~
— false~string~
null~string~(2)
— null~object~
— null~string~
t~string~(2)
— testArray,testArray~object~
— t~string~
false~string~(2)
— false~boolean~
— false~string~
testArray,testArray~object~(1)
— testArray,testArray~object~
null~object~(2)
— null~object~
— null~string~
testArray,testArray~object~(2)
— testArray,testArray~object~
— t~string~
~ keys count ~(7)
~ all vals count ~(13)
wdyt?
#24 by jackson on July 29th, 2011 ·
That’s definitely a way to get rid of the duplicates. You could also use the class (accessible via var[“constructor”]) as the key. However, it seems like a lot of extra
Dictionary
access as well as callingtypeof
. Would you care to test the performance against the version from the article? I’d be interested to know how much of a slowdown there is in order to get cached counts (for strong keys, at least).#25 by jpauclair on August 1st, 2011 ·
(var as Object).constructor
That’s how I do in FlashPreloadProfiler
#26 by lyua on August 2nd, 2011 ·
yes it could be used, but if
key
orvalue
come to insert*() methods as anull
it will be run-time error:__map.insertVal( null, null); __map.insertVal( null, "null"); __map.insertVal( "null", null);
#27 by lyua on August 3rd, 2011 ·
Hi. Detail results are below. Here are a short conclusions:
1) all counts methods work significantly faster in Lyua version;
2) all others (insert, has) work slower or the same;
3) slow work of insert method is not critical: for 8000 different records JD version spent 7 mlsec, Lyua version 9 mlsec. Fro using in real application both of them cost nothing (of course, Lyua version lets to avoid duplications and some edges, like “false”, and false, etc.);
4) the same could not be said about count methods, because counting for 3.7 seconds is a showstopper for using JD version. Around the same for removePair() (2 seconds for 8000 records for JD version and 18 mlsec for Lyua version).
5) Lyua version must not use for weak reference!!!
Function, JD, Lyua, Relative diff
run#1
insertVal, 7, 9, 1.28
hasKey, 1, 4, 4
hasVal, 3, 6, 2
countAllVals, 3790, 1, 0.00026
countVals, 3701, 4, 0.00108
remove, 1990, 18, 0.00904
run#2
insertVal, 7, 10, 1.43
hasKey, 2, 4, 2
hasVal, 2, 7, 3.5
countAllVals, 3702, 1, 0.00027
countVals, 3691, 4, 0.00108
remove, 2021, 17, 0.00841
run#3
insertVal, 8, 9, 1.125
hasKey, 2, 4, 2
hasVal, 3, 6, 2
countAllVals, 3765, 1, 0.00026
countVals, 3714, 5, 0.00134
remove, 2000, 18, 0.009
I ran the performance test with the following environment:
Flex SDK 4.5.0.19768, compiling in release mode
Version of Flash Player 10.2.159
2.0Ghz Intel Core2 4400
Windows XP, SP3.
Test code is here.
Source code of LyuaMultiMap:
#28 by jackson on August 3rd, 2011 ·
Looks like you have a good alternative provided that:
weakKeys
parameter?)Dictionary
objects being createdinsertVal
being 27% slower,hasKey
being 2.4x slower, andhasVal
being 2.375x slower (on average)Since there’s no clear winner, it would seem that the choice of multi-map is up to each user. If counting is important to the user, then your version seems like a good option. :)
#29 by lyua on August 4th, 2011 ·
I completely agree with you, that if we need weak reference it should use JDMultiMap. Then, if we are going to use multi-map for ordinary code, it prefer to use LyuaMultiMap, since it avoids duplicates, fastest for counts, and not slow significantly for insert*() and has*() operators (sure I don’t see big difference between 7 and 9 milliseconds for 8000 operations!), and finally, if we are going to use insert*() or has*() in onEnterFrame() or something like that, it brings some advantages when use JDMultiMap, if we are not going to to use count*() on so frequently called handlers.
BTW, if we goes with 30 fps, for example, for a 1 hour, it makes around 108,000 iterations; see some results below, it just show that you approach win only 150 milliseconds during of 1 hour.
(ran from the Flash Builder shell with debug player!)
Function, JD, Lyua, Relative diff
@8,000
insertVal, 12, 21, 1.75
hasKey, 5, 9, 1.8
insertVal, 13, 20, 1.53
hasKey, 6, 9, 1.5
@80,000
insertVal, 89, 197, 2.21
hasKey, 53, 86, 1.62
insertVal, 97, 197, 2.03
hasKey, 55, 82, 1.49
@108,000 // 30 fps for 1 hour
insertVal, 114, 265, 2.32
hasKey, 73, 120, 1.64
insertVal, 125, 277, 2.22
hasKey, 72, 111, 1.54
@800,000
insertVal, 767, 1966, 2.56
hasKey, 551, 828, 1.50
insertVal, 781, 1956, 2.50
hasKey, 539, 838, 1.55
@8,000,000
insertVal, 7800, 19770, 2.53
hasKey, 5279, 8292, 1.57
insertVal, 7660, 19752, 2.57
hasKey, 5331, 8232, 1.54
#30 by jackson on August 4th, 2011 ·
“30 fps for 1 hour” is really just one multi-map usage per frame though, which means that performance is almost irrelevant. Say you were doing 8,000 multi-map operations per frame. In your test above, the version from the article would save you about 2 milliseconds per frame, which is a lot when you consider that 30 FPS is only 33 milliseconds per frame. On a slower machine (e.g. netbooks, mobile, older desktops), it may be a difference of 4-5 milliseconds.
Again, I think there are good uses for your version—avoiding duplicates and fast counting—but there is a real-world price to pay that each user of the multi-map must bear in mind when choosing an implementation. Our versions are just optimized for different use cases. :)
#31 by Rafael Rinaldi on July 26th, 2011 ·
That’s really cool and useful. I wrote something similar to this, check it out: http://github.com/rafaelrinaldi/list
#32 by jackson on July 26th, 2011 ·
The link you posted seems to be a linked list implementation, not a multi-map. Am I looking in the wrong place?
#33 by Rafael Rinaldi on July 26th, 2011 ·
No, you’re not wrong. As I said is just similar.
#34 by Gunnar Karlsson on September 24th, 2011 ·
@Jackson. This is a really useful class – thank you for making it available. Maybe this is a silly question but I’m curious why you use double underscores for the private class properties (as opposed to single)?
#35 by jackson on September 24th, 2011 ·
Glad to hear you like it. :)
I picked up the double underscores naming convention during the AS1/AS2 days when Adobe was naming public properties with single underscores to avoid naming conflicts. Personally, I also find it easier to type than “m_X” or “mX” as in other naming conventions, but perhaps I should have switched to single underscores or “no prefix” when AS3 was introduced. I tend to not like the “no prefix” version though as some nasty scoping issues come up:
I think that’s getting pretty confusing compared to a naming convention:
Everything seems clearer (to me!) in that version except the
this.value
insub
, but you wouldn’t write that expecting to assign to the member because it’s name is__value
.Also, the “no prefix” approach makes it harder to make getters and setters:
In summary, I dislike the “no prefix” approach. Perhaps a better naming convention (single underscore?) could be adopted, but after five years it almost seems too late.
#36 by Gunnar Karlsson on September 24th, 2011 ·
@jackson -thank you for explaining, and with such good examples.
I have a question regarding your Multimap class. If I wanted to iterate through the keys to display them in a list, but I don’t know how many keys there are or what the keys are, how would I do it? This would useful if I work with a web service API where I have to collect the keys from each VO to display them since the API doesn’t allow me to get a list of all keys. I’ve stumbled upon this issue a couple of times. Thankful for your perspective.
#37 by Gunnar Karlsson on September 24th, 2011 ·
@jackson – addition to my last post:
The reason I ask is that the Dictionary class doesn’t from what I can tell allow iteration over its keys and your MultiMap class uses Dictionaries as return values.
#38 by jackson on September 24th, 2011 ·
You can iterate over the keys of a
Dictionary
like this:Or you can iterate over the values like this:
The
*
type can be anything, which is good because the keys and values ofDictionary
can be anything.Happy iterating!
#39 by Gunnar Karlsson on September 24th, 2011 ·
@jackson
var aMillion:int = 1000000;
for(var i:int = 0; i < aMillion;i++){
trace("thank you!)
}