In the previous episode, it appeared that Delphi XE2 64bit compiler was achieving quite good results, however, after further investigations, things may not be so clear-cut. Transcendental maths, which will be food for a another post, the subject of this one seems to be an issue with single-precision floating point maths.
For a first look I’ll reuse a Mandelbrot benchmark, based on this code Mandelbrot Set in HTML 5 Canvas. What it tests are double-precision floating-point basic operations (add, sub, mult) in a tight loop, there is relatively little in the way of memory accesses (or shouldn’t be, to be more accurate).
A useful, yet not very prominent class of DWScript is TdwsSymbolDictionary. It provides a reference of all symbols mentioned in your script, where they are declared, where they are used, etc. It is optionally filled up when you compile a script with the coSymbolDictionary compile option, and can be accessed from a compiler program object or interface.
It can be used for multiple purposes, centered around IDE features and auditing. Here are a few usage scenarios.
In-place symbol help and navigation
The most basic use of TdwsSymbolDictionary is probably to figure out which symbol is under the cursor or the mouse in your code editor. You can then use the symbol to provide relevant contextual help (either from your help file, or auto-generated from the script, such as a list of parameters for a function, of fields for a class, etc.) or look up the dictionary once more, and provide information about where it’s declared, implemented, used or forwarded. That positional information can also be used to offer navigation shortcuts between declaration and implementation, or to any of the symbol’s occurrences in the source.
The TSymbolDictionary can be used it to figure out what is currently being typed, by identifying the symbol at the cursor, or the last recognized symbol in the code being entered. From that point on, you can generate a list of contextual variables, methods, fields, etc. that can be used to fill up an auto-completion drop down.
Once you have a symbol pinpointed, either by name or through one of its occurrences in the source, you have access to all the places it’s occurring in the code. A trivial use of such a list is for a rename refactoring: as the list of symbol positions is already sorted by source file, line and column, you merely have to walk the list backwards and replace occurrences in your sources files.
You can also team up TdwsSymbolDictionary with other information sources to implement more complex refactorings. For instance alongside TdwsSourceContextMap (which is also provided in a compiled program), you can implement “extract method” (or its variants, “pull up” & “push down”).
The symbol dictionary can be a convenient starting place for symbol-related audits and diagnostics. All audits that relate to the amount of times a symbol is used (or not) being the most trivial.
Another straightforward usage is when you have naming conventions for fields, variables, classes etc. and/or to enforce case consistency. The later can be handled in your IDE in a similar fashion as a rename refactoring, except you only replace in suReference and suImplementation symbol positions, and it is harmless enough you can do it in a background task without requiring user interaction.
You can also use it to diagnose code issues, for instance such patterns:
someObject.someProperty[stuff].otherProperty.function.method(1); someObject.someProperty[stuff].otherProperty.function.method(2); someObject.someProperty[stuff].otherProperty.function.method(3);
can be heuristically detected (either as “too many member symbols on the same line” or “member symbol repeated too much in nearby lines”), and the attention brought to the hot-spots so the code can be simplified and cleaned up.
Sometimes, the most simple-looking code can cause the Delphi compiler to stumble.
I bumped on such a case recently, and simplified it to a bare-bones version that still exhibits the issue:
A compiled script, a TdwsProgram, cannot be saved to a file, and will not ever be. Why is that?
This is a question that dates back to the first DWS, as it was re-asked recently, I will expose the rationale here.
- DWS has a very fast compiler, there are no performance problems compiling scripts instead of loading a binary representation that has to be de-serialized. How fast is it? See below.
- DWS lets you define custom filters, that enable you to encrypt your scripts easily, if hiding the script source is what you were after with the bytecode.
- DWS compiler/parser portion is quite light (currently less than 75kB), especially compared to the size of the Delphi libraries you will be using for the runtime. You probably will not notice it in the EXE size once you expose more than a few trivial libraries.
- Last but not least, when loading a binary representation of a script, you have to make sure all libraries are compiled into the application that loads and wants to execute the script, and that they are entirely backward-compatible with what was exposed to the script back when it was compiled. That is irrelevant when re-compiling.
How fast is the DWS compiler?
I did some quick benchmarking against PascalScript and Delphi itself.
I generated a script based on the following template:
var myvar : Integer; begin myVar:=2*myvar-StrToInt(IntToStr(myvar)); end;
The assignment line being there only once, 100 times, 1000 times, etc. The result was saved to a file, and the benchmark consisted in loading the file, compiling and then running it for DWS. For PascalScript, the times are broken down into compiling, loading the bytecode output from a file, and then running that bytecode. Disk size indicates the size of the generated bytecode.
All times are in milliseconds (and have been updated, see Post-Scriptum below):
For line counts expected for typical scripts (less than 1000), compared to PascalScript, the cost of not being able to save to a bytecode is a one-time hit in the sub-15 milliseconds range, on the first run.
This illustrates why it is not really worth the trouble maintaining a bytecode version for scripting purposes, and that is also my practical experience.
For larger scripts, it is expected the execution complexity will dwarf the compile time: the benchmark code tested here doesn’t have any loops, anything more real-life will have loops, and will likely have a greater runtime/compiletime ratio.
What of Delphi?
For reference, I tried compiling the larger line counts versions with Delphi XE, from the IDE.
- the 100k lines case took 3 minutes 27 seconds to compile (ouch!), obviously hitting some Delphi parser or compiler limitation. Runtime was 63 ms.
- the 10k lines case in Delphi compiled in a more reasonable 2400 msec, and ran in 4 ms (50% faster than DWS).
What else? The DWS compiler has an initial setup cost higher than PascalScript, but as code size grows, it starts pulling ahead. That setup overhead will nevertheless bear some investigation 😉.
Once compiled, the 10x execution speed ratio advantage of DWS vs PascalScript is consistent with other informal benchmarks.
Gave a quick look at the setup overhead with SamplingProfiler, and found two bottlenecks/bugs. The outcome was the shaving off of 3 ms from the DWS compile times, ie. the compile times for the 1, 100 and 1000 lines cases are now 0.95 ms, 2.85 ms and 19.1 ms respectively.