On Monday, 4 June 2012 at 19:18:32 UTC, Dmitry Olshansky wrote:
On 04.06.2012 22:42, Roman D. Boiko wrote:
... it is possible to create persistent (immutable) tries
with efficient (log N) inserting / deleting (this scenario is
very important for my DCT project). Immutable hash tables
would require 0(N) copying for each insert / delete.
Aye, the good thing about them - the amount of data affected by
any change is localized to one "page". So you just copy it over
and remap indices/pointers.
Actually, Microsoft went this way (immutable hash tables) in
their Roslyn preview implementation. However, I still believe
that tries will work better here. Will check...
Would bulk pre-allocation of memory to be used by trie improve
locality? With some heuristics for copying when it goes out of
control.
It is difficult to create a good API for fundamental data
structures,
because various use cases would motivate different trade-offs.
The same
is true for implementation. This is why I like your decision to
introduce policies for configuration. Rationale and use cases
should
help to analyze design of your API and implementation, thus
you will get
better community feedback :)
Well I guess I'll talk in depth about them in the article, as
the material exceed sane limits of a single NG post.
In brief:
- multiple levels are stored in one memory chunk one after
another thus helping a bit with cache-locality (first level
goes first)
- constructors do minimize number of "pages" on each level by
constructing it outwards from the last level and checking
duplicates (costs ~ O(N^2) though, IRC)
So this price is paid only on construction, right? Are there
alternatives to chose from (when needed)? If yes, which?
- I learned the hard way not to introduce extra conditionals
anywhere, so there is no "out of range, max index, not
existent" crap, in all cases it's clean-cut memory access.
Extra bits lost on having at least one "default" page per level
can be saved by going extra level
Could you please elaborate? How do you handle situations when not
existent, etc., is needed?
Your examples deal with lookup by the whole word (first/last
characters and length are needed). Are your API and
implementation adaptable for character-by-character trie
lookup?
I would say that one by one won't help you much since the speed
is almost the same if not worse.
I guess, in general your statement is true, especially because
known length could improve speed significantly. Not sure (but can
easily believe) that in my particular situation it is true. For
example, one by one would allow ignoring key encoding (and thus
using multiple encodings simultaneously just as easily as single).
The problematic thing with one by one - say you want to stop
early, right?
Why? I'd like to lex inout as TokenKind.InOut, not TokenKind.In
followed by TokenKind.Out. Did I misunderstand your question?
Now you have to check the *NOT FOUND* case, and that implies
extra branching (if(...)) on _each level_ and maybe reusing
certain valid values as "not found" marker (extra
configuration?).
This should be easy, if something is not a keyword, it is likely
an identifier. But I agree in general, and probably even in my
case.
Branching and testing are things that kill speed advantage of
Tries, the ones I overlooked in my previous attempt, see
std/internal/uni.d.
The other being separate locations for data and index,
pointer-happy disjoint node(page) locality is another way of
the same fault.
This concern disturbs me for some time already, and slightly
demotivates, because implementing something this way will likely
lead to a failure. I don't have enough experience with
alternatives to know their advantages and trade-offs. I'll check
your code. I did plan to try table lookup instead of branching. I
guess making my own mistakes is necessary anyway.
Will compile-time generation of lookup code based on tries be
supported?
Example which is currently in DCT (first implemented by Brian
Schott in
his Dscanner project) uses switch statements (which means
lookup linear
in number of possible characters at each position).
Nope, the days of linear lookup for switch are over (were there
even such days?) compiler always do binary search nowadays if
linear isn't more efficient e.g. for a small number of
values.(it even weight out which is better and uses a
combination of them)
I thought this *might* be the case, but didn't know nor checked
anywhere. I also wanted to do linear search for some empirically
chosen small number of items.
However you'd better check asm code afterwards. Compiler is
like a nasty stepchild it will give up on generating good old
jump tables given any reason it finds justifiable. (but it may
use few small jump tables + binary search, could be fine... if
not in a tight loop!)
Thanks.
A trivial
improvement might be using if statements and binary lookup.
(E.g., if
there are 26 possible characters used at some position, use
only 5
comparisons, not 26).
Moreover you'd be surprised but such leap-frog binary search
looses by a big margin to _multilayer_ lookup table. (I for one
was quite shocked back then)
Thanks again :) Are any numbers or perf. tests available?
I wanted to analyse your regex implementation, but that's not
an easy
task and requires a lot of effort...
Yeah, sorry for some encrypted Klingon here and there ;)
It looks like the most promising
alternative to binary trie lookup which I described in previous
paragraph. Similarities and differences with your regex design
might
also help us understand tries better.
Well if you are in no rush you might as well just take my
latest development in the ways of Trie from my gsoc fork :)
Skipping some gory days of try and trial (fail) in the process
;)
OK