Hi,
I'm a bit skeptic, because 10x improvement is pretty hard to get if the benchmark is not flawed and the code doesn't use VM support.
Okay, I found the benchmark code, and it's flawed. The distribution of the keys is not uniform. Actually it's far from uniform, it's just 1..dictSize. Anyway I ran the benchmarks in Squeak 4.2 alpha and got the following results
...
The PDictionary is only better at 2 benchmarks:
Includes (not IncludesKey!) and RemoveKey which are both rarely used. In all other tests Squeak's Dictionary implementation is faster in this flawed benchmark.
I agree this benchmark is severely flawed. But it's generally also the ideal case for Pharo/Squeak dictionaries. Whatever might happen in other cases to degenerate the dictionary doesn't happen here since keys are added in the right order (increasing hashes). So whenever place should be made for a key with an already existing hash in Dictionary, this is avoided. In PDictionary however this scenario wouldn't even happen. Now the problem is that I don't have experience in writing proper benchmarks and this thread could provide us with some decent benchmarks that better show standard use of dictionaries (and as I hope, show that PDictionaries are more resistant to degradation than the current Dictionaries.

What we should take away from the existing benchmarks is that the differences aren't even that high for the ideal case of Pharo/Squeak dictionaries.

The main point in favor of the PSet / PDictionary implementations is that you always know the exact ranges of keys/values that belong to a certain hash. So for includesKey: and at:ifAbsent: this is ideal since you never look further than the current hash-value. In Squeak/Pharo dictionary/set you don't know this, thanks to hash-collision/propagation, so you have to scan until you find an association that is nil. If you are unlucky and your whole dictionary has all the associations grouped together, even though they all represent keys with different hashes, you'll have to look through a whole bunch of totally unrelated keys.

For example, I quickly changed one of the benchmarks to be a bit less optimal for the Squeak/Pharo collections, namely the includesKey: benchmark (which is also representative for at:)
Rather than doing

    1 to: dict size * 2 do: [ :i|
        dict at: (self key: i) ifAbsentPut: (self value: i)].

I changed it to:

    1 to: dict size * 10 by: 10 do: [ :i|
        dict at: (self key: i) ifAbsentPut: (self value: i)].

This forces the dictionary to miss a lot more often. Since all the keys are grouped together this is the worst case for Squeak/Pharo dictionaries if you want to look something up that's not there. This are the results on Squeak 4.1:

PBDictionary  0.0261 +/-0.0017
PBSTDictionary  5.235 +/-0.01

So around 200x faster in case of PDictionary.

While PDictionary stays stable (even slightly faster, go figure), Squeak dictionaries degrade immensely! On Pharo the results are a bit better, but still very much degraded:

PBDictionary 0.02387 +/-0.00014
PBSTDictionary 1.6577 +/-0.0036

So around 60x faster in case of PDictionary.


If on Set I ensure that all elements will be found, I get the following result for #includes:

PBSetIncludes 0.00440 +/-0.0001100000000000001
PBSTSetIncludes 0.004000 +/-9.6e-5

making PSet marginally slower. However, once you start not finding elements, with the same change as before, (to: size*10 by: 10):

PBSetIncludes 0.00443 +/-0.0001
PBSTSetIncludes 0.9844000000000005 +/-0.0017

So around 220x faster in case of PSet. While PSet and PDictionary never really suffer from collisions, normal Squeak/Pharo dictionaries suffer immensely and grow up to 200x slower. Btw, this last benchmark was run on Pharo.


Anyway, you are definitely right that the benchmarks suck and that we need better ones.

As for PDictionaries and PSets, nowing that you know exactly what the range of existing hashes is I think it's only logical that it will be faster in many of the operations that need this knowledge, such as #includesKey:, #remove:, #at:put:/#add: and #at:/#includes:.

Since PDictionary switches between SmallDictionary style and normal dictionary style, it also seems logical that it behaves faster in case that you are in SmallDictionary style without having to care about it. This is also not shown in any benchmark, but it is the case and makes it faster for that case. However, this added test makes it slower for the general dictionary style so removing it might improve things in that case to regain the marginal difference between both dictionaries in the ideal case for Pharo/Squeak.

There is anyway no reason why PDictionary would be slower than Pharo/Squeak dictionaries except for design decisions like the previously mentioned one, and those can be changed if wanted. There is however a good reason why Squeak/Pharo dictionaries are slower and degrade faster, namely colliding hashes that even propagate collisions (also not benchmarked at the moment!).

Ok, on the whole I wasn't really planning on pushing these dictionaries too much on Pharo / Squeak. We use them in Pinocchio and are happy with them (especially since we have native support). But they are there and can be used if you want to. I can understand that people are skeptical, but since I don't really have knowledge on building benchmarks (I didn't even set up the current ones, it was a student of mine) I don't think I can help much there. We use them throughout our project and some parts (such as our parser) have gotten a lot faster since we started using them.

Btw, the code would indeed just be released under MIT license which is maybe already automatically the case since I signed that agreement as a contributor to Pharo?


Some answers / comments to the other random questions and remarks:
I wonder what the #pPrimitive:plugin: pragmas stand for in PDictionary's
#at:ifAbsent: and #at:put:.
Those methods are implemented as primitives in the Pinocchio VM that we are developing. In primitive form most of the methods run ~ twice as fast. This is however not compatible with the way the stack is handled in Squeak/Pharo so can't really be ported.
The current HashedCollections don't shrink. Remove is a rarely used operation.
Ok, makes sense.
One could implement the current dictionaries without associations, that's just less object-oriented. That would generate even less "garbage".
True enough.
Let's imagine that we want to put 10000 objects into a IdentitySet. Each object has it's identityHash in the 0..4095 range. The hash function is very simple: hash \\ capacity + 1. Therefore the values of the hash function will all be in 1..4096, even if capacity is ~15000. This causes a lot of collisions (the table will be consist of a few very long chains degrading performance to unacceptable leves). These collisions are avoided by the shift.
So you change the hash implementation to ensure that they take up the whole array of the Dictionary rather than just the first 4096 + collisions positions. Makes perfect sense for the squeak/pharo implementation but rather strange to enforce this on the whole system imho.


Ok, I hope this clears up some things.

cheers,
Toon

_______________________________________________
Pharo-project mailing list
[email protected]
http://lists.gforge.inria.fr/cgi-bin/mailman/listinfo/pharo-project

Reply via email to