-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add benchmarks #165
Comments
Absolutely! It's all open source 😄 |
Thank you! To recap, it seems you:
Is that correct? |
Top down it's rather so:
A parser run consists of n loops, determined by the
|
Thanks! I've managed to emulate the same workflow in Kotlin, and now I find myself dealing with a stack overflow on the JVM with this statement:
Removing it completes the run successfully tho. |
Yes, that's a tough one. When parsing this in a thread in C++ it crash that thread unless you are on a platform that allows to set the thread stack size. This is a big problem in ANTLR as crashing a thread usually crashes the entire app (nice DDOS attack)! The problem is the unlimited lookahead and the way prediction is implemented (as recursion instead of iteration). And additionally, expressions in MySQL (and many other languages) are highly nested so one opening par alone can result in a dozen or more sub rule invocations. That's why I love this one as a stress test. Can you increase the stack size during execution? |
btw. please share numbers once you got it all running! And while you are on it, can you also run it against plain Java? I'm really interested in comparing everything. |
Some runtimes have methods that could check if there is enough free space in thread stack for one more recursive call. For example, EnsureSufficientExecutionStack in C#. In theory, such check can be integrated into targets that support such feature. For other runtimes, it's possible to use manual recursive call counter and calculate its limit empirically (maximum number of recursive calls). Another way is to emulate recursive calls using separated stack. But it requires much more implementation effort.
Yes, it should be possible, but it's not a reliable resolving of the problem. SO exception should not occur in any case. |
The root cause should be removed instead of all these workarounds. In my next generation ANTLR project I plan to convert the recursive prediction code to become iterative and add an iteration limit. This has been requested before, to limit lookahead to some max value. |
Preliminary benchmark results on a i7-7820HQ - 32GB
|
I think on K/JS the major performance issue is the use of a string for the generated ATN, which must then be converted to a char array, and decoded to an int array. The JS and TS runtimes use an array of integers directly. @KvanTTT not sure if we can do something to differentiate based on the platform. |
@mike-lischke About your NG project, do you think that some ideas could be contributed to the just started effort on ANTLR 5? |
Yes, it's a good solution, I also said about that.
I'm not sure it's a hot path since this String is being deserialized only once on app startup. But anyway I think using array of Int instead of String is a better option (it requires some effort on JVM side because it crashes on big arrays, but it's possible to split one huge int array to several more smaller ones).
It's a great idea but I'll email you about the current situation (not sure it's public info). |
True, unfortunately JVM can't handle huge arrays unlike other runtimes out of box. But it's probably possible to use direct memory or splitting into several smaller arrays. Actually, this array is only required only once during ATN deserialising and there is no need to keep it in memory.
What's about other parsing stages? Probably they consume much more time. |
The different machines might have an impact here. Have you tried running the pure JS benchmarks? My box is a Mac Studio M1 Max - 32 GB. |
I don't think this is a real issue, given that this happens only once per parser/lexer class load. That would impact only the cold run time. Once the parser is warm the ATN is in memory. |
It was a little surprising to me that ANTLR5 got started. But that's so great about open source: everyone can do what he/she likes. I prefer TypeScript and have (mostly) other goals than ANTLR5. I'm on the way to radically simplify ANTLR (just today I brought the TS runtime tests to under 10 seconds, just by reorganizing things). |
Really? You can just allocate an array up to 2^31 entries (more than enough for the ATN). But I'm not a Java expert so...
ANTLR started out with split arrays :-D A while ago there were sections that got concatenated at load time. Using a string however is all but optimal, as it involves charset related functionality for conversion. Still, it's not a hot path and hence not worth much work to optimize. |
@mike-lischke you were right about machine differences. I've cloned your repo and run the benchmarks. |
I also have wondered by there was a switch to using string, if we could just split the arrays. |
I don't remember why it was implemented in that way, maybe it was just more easier. I've found a related PR: antlr/antlr4#3591 |
I'll open a PR as soon as the others are merged, just to avoid too much stacking. I've removed all parse error catching, so if it fails it's unrecoverable, and I've not encountered any failures (apart from the stack overflow). Benchmark times are consistent, as per updated table. |
You mean all catch clauses in generated code? Interesting idea, but besides the fact that recovery is no longer possible (is this really needed in everyday applications?) you also cannot do the double parse approach use LL and SLL prediction modes with the bail out error strategy. |
Oh no no. Initially I had copied the try-catch you have in the parsing service, which accumulates errors. Now I don't catch any exception in the benchmarking code. |
I've just added a benchmark for the WASM ( Overall it seems WebAssembly on Node.js cuts the times in half or more compared to JS. |
Interesting numbers! If you could add C++ and C# (and maybe Go) too this would be the long awaited performance comparison chart for the ANTLR runtimes :-) Can you explain more how the WASM target is structured? Is it just the runtime, used with a Java/Kotlin wrapper or is entirely compiled to a WASM module, including the generated parser/lexer? How's the WASM used then? And another one: the JS and WASM output are both generated from Kotlin code? Btw. I'm surprised you can generate useful WASM code from Kotlin, given that this is marked as being in alpha state. |
The runtime and the benchmark code are both compiled down to optimized WebAssembly binaries. Kotlin outputs a small JS wrapper as entry point, which will load the WASM binary: if (isNodeJs) {
// ...
const wasmBuffer = fs.readFileSync(path.resolve(dirpath, wasmFilePath));
const wasmModule = new WebAssembly.Module(wasmBuffer);
wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
}
Yes. Even tho the WebAssembly target is in alpha, the target itself has been around for quite some time, so I think the accumulated experience helped in getting it to work very well immediately. The benchmark results are in line with what has been shown here. |
OK, thanks, that explains the good performance. Since Kotlin itself compiles to WASM, it makes no difference for the developer who's working with Kotlin code. However a, say, C++ developer, who wants the parser code to be in C++ cannot use this WASM binary, right? Same for any other language but Kotlin. Is that the correct understanding of the setup here? |
You can use the binary if you have a runtime for WASM, which is language/platform dependent. Now, I'm not sure if the exported functions can actually be called by all runtimes, but I think it's going to be a yes in the future. |
IIUC the generated parsers, lexers, visitors and listeners are also part of the WASM binary, correct? If so that would be contrary to the idea of having these files in the target language. |
Yes it's correct.
In the case of this repository, I very doubt non-Kotlin consumers exist. This exists mainly to allow Kotlin Multiplatform consumers to compile their projects to the 25 available platforms, and expose a small wrapper API to the outer world if necessary. This is what I do in my projects, I expose a ANTLR 5 goal seems similar, which is to allow ANTLR consumers to generate an all-in-one bundle that can be run by all the platforms/languages that offer a WASM runtime. Currently Kotlin/Wasm offers very limited exportability of definitions to JS, e.g., you cannot export classes, but only functions, and you cannot export non-JS native or non-primitive types. |
Thanks Edoardo, yes I know what a pain this is currently. Last summer I tried to wrap the C++ runtime in a WASM binary (and only that), which requires to export classes so that the target code that uses this binary can still have their parsers etc. in their preferred language. This approach was pretty slow, slower than pure JS (which made me switch to the JS runtime and create a TS version from it). The idea of having a single WASM runtime for all ANTLR targets is appealing, but not very realistic, because it requires that parser writers would always have to run Kotlin as part of their build pipeline. A language they are even less familiar with than Java (if at all). You cannot debug your generated files in this scenario and cannot use action code in your target language. For the Kotlin target no problem at all, but for all other ANTLR target languages. |
I guess in an ideal scenario the Kotlin runtime to WASM compilation process would produce something on the line of TS type declarations. Those type declarations would allow other languages compiling down to WASM to generate language-specific bindings. Edit: well that also means ANTLR would provide a way to generate code in multiple languages, as it does now. |
Kotlin also has Native target. But I'm not sure if it's convenient to bind C++ and Kotlin Native output. At least it's possible to generate dynamic libraries: https://kotlinlang.org/docs/native-dynamic-libraries.html |
It's correct but it's unlikely ordinary user will debug parsing code if even it's generated.
Yes, but target-language actions are quite painful, since they require grammar duplication for different targets or other tricks. Also, different runtimes has a lot of common logic, but it's tough to support them all in actual state. Honestly, I'm not sure how to resolve this problem in the best way, but I would like to try Kotlin and yes, I'm a bit biased with Kotlin since I work on its compiler. |
@mike-lischke this is what will be needed to implement the quoted part: https://github.com/WebAssembly/component-model |
Reopened as suggested by @lppedd |
I'm trying to benchmark Native on |
It's failing on
I suppose this requires deep recursion, so maybe Native has some limitations? |
Well, there is really no point in benchmarking Native. Native seems feasible for small grammars, but at the size of the MySQL one, I'd avoid it. |
Ok, partially my fault here. I didn't enable optimizations. I've updated the results table, and while it got noticeable better, it's still way behind the other platforms. |
Just as a note, JS and Native are very sensitive to non-null assertions ( |
I guess this is relevant only to generated code from Kotlin, right? |
@mike-lischke yup! You won't have this issue in TS. And on the JVM the cost is pretty much zero. |
I've benchmarked the difference between deserializing an ATN directly as a Java performs slightly worse when accessing characters via the I've also tried going back to the |
Thanks for detecting the problem. Could you share this generated parser? |
I'd like to setup a couple of benchmarks using kotlinx-benchmark.
The benchmarks should measure something we can compare with other ANTLR implementations, e.g., antlr4ng.
@mike-lischke hey! Would you be ok for us to reproduce the same benchmarks for antlr-kotlin?
The text was updated successfully, but these errors were encountered: