This sample is a port of the original “Nabialek trick” from classic Spirit. See the cpp file here.
Thomas Bernard did some performance investigations and concluded that the older version of this article which stores the rules directly in the symbol-table is sub-optimal. This updated article uses rule pointers instead.
I’ve done some more investigation on the performance issue the proposed Nabialek trick implementation shows. With the help of callgrind I confirmed that the main performance hit comes from copying the parser out of the symbol table when a successful parse occurs into the rule local variable. (If anybody is interested I can provide the callgrind results)
There is a very simple way to prevent that: use pointers. This time the optimization I propose works
The modifications to the proposed implementation are simple:
- Replace the symbol table definition to use rule pointers instead of normal rules
- Replace the rule local variable type to be a pointer to a rule instead of a rule
- Adapt the call to the lazy parser to take into account the modified local variable type
Using pointers prevents copying the rules around and gives about three times faster parsing speed in the most simple cases. The speed improvement can be much bigger as the complexity of the rules stored in the symbol table increases.
This technique, I’ll call the “Nabialek trick” (from the name of its inventor, Sam Nabialek), can improve the rule dispatch from linear non-deterministic to deterministic. The trick applies to grammars where a keyword (operator, etc), precedes a production. There are lots of grammars similar to this:
r = keyword1 >> production1 | keyword2 >> production2 | keyword3 >> production3 | keyword4 >> production4 | keyword5 >> production5 /*** etc ***/ ; ;
The cascaded alternatives are tried one at a time through trial and error until something matches. The Nabialek trick takes advantage of the symbol table’s search properties to optimize the dispatching of the alternatives. For an example, consider this grammar:
one = id; two = id >> ',' >> id; start = *("one" >> one | "two" >> two);
There are two rules (one and two). When “one” is recognized, rule ‘one’ is invoked. When “two” is recognized, rule ‘two’ is invoked.
Here’s the corresponding grammar using the Nabialek trick (see nabialek.cpp):
one = id; two = id >> ',' >> id; keyword.add ("one", &one) ("two", &two) ; start = *(keyword[_a = _1] >> lazy(*_a));
The grammar works as follows. keyword is a symbol table with two slots containing rule pointers:
qi::symbols<char, qi::rule<Iterator, ascii::space_type>*> keyword;
This is initialized to contain the address of rules “one” and “two”.
one, two, id, start are rules which are declared as follows:
qi::rule<Iterator, ascii::space_type> id, one, two; qi::rule<Iterator, ascii::space_type, qi::locals<qi::rule<Iterator, ascii::space_type>*> > start;
The start rule has a local variable —a rule pointer. In this example, when we enter the start rule, we create a temporary local variable. When a valid keyword is matched, notice that the semantic action stores a pointer to the rule we initially stored in the symbol table into the local variable with this code:
keyword[_a = _1]
_a refers to the local variable and _1 refers to the attribute of keyword —the rule pointer contained in the symbol slots.
Now that the local variable (referred to by _a) contains a pointer to the rule we want to continue parsing with, the next thing to do is call it. This is done by the lazy pseudo parser:
The dynamic grammar parses inputs such as:
"one only" "one again" "two first, second"
The cool part is that the rule we want to parse with is dynamically chosen at runtime depending on what the symbol table contains. If it got a “one” then we use the ‘one’ rule. If it got “two”, then we use the ‘two’ rule. Very nifty! This technique should be very fast, especially when there are lots of keywords. It would be nice to add special facilities to make this easy to use. I imagine:
r = keywords >> rest;
where keywords is a special parser (based on the symbol table) that automatically sets its RHS (rest) depending on the acquired symbol. This, I think, is mighty cool! Someday perhaps…
Now, that last line was written in 2003. Will someone come forward and finally do it?