Skip to content
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

Improve the performance of the GDScript VM #6031

Open
reduz opened this issue Jan 1, 2023 · 239 comments
Open

Improve the performance of the GDScript VM #6031

reduz opened this issue Jan 1, 2023 · 239 comments

Comments

@reduz
Copy link
Member

reduz commented Jan 1, 2023

Describe the project you are working on

Godot

Describe the problem or limitation you are having in your project

Many users have complained for a long time about the performance in GDScript.

For the GDScript 2.0 rewrite, a lot of work was put into making sure the VM has all the information required to generate optimized code, including type information, strict type checking, better addressing modes and the ability to access every operator, utility function, etc. as a C function pointer. This can be evidenced by the new codegen class used to emit code.

Unfortunately, just the changes to the parser and compiler ended up being very significant so almost no optimization work could take place for the VM.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

The idea is to use the the new type information as well as all the C function pointers to engine to generate direct executable code and bypass the VM, allowing for much greater performance when typed code is used.

This is challenging to do in a practical way, however. Because of it, this is more of an open discussion on evaluating different alternatives.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

There are a few general approaches that could be taken to optimize GDScript:

  • JIT
  • real-time AOT
  • offline compilation.

The problem with JIT and real-time AOT is that there are many platforms that do not support real-time code generation, such as iOS, WebAssembly or consoles. Because of this, if we followed this approach, Godot would not run well on those.

As a result, the ideal way to compile GDScript code to binary would be to compile the whole script to a shared object (DLL on Windows, .so on Linux, .dylib on MacOS, etc) that can be bundled together with the exported game.

Fortunately, GDExtension provides pretty much everything the compiled script needs:

  • A standard C ABI that allows accessing all the engine API and C function pointers.
  • The means to bundle the shared object with the game on export.

The question that remains is how to build cross-platform code from the editor. There are a few alternatives to doing this:

  • Require the user to install an external compiler (like GCC or LLVM) and basically generate the C code for the GDExtension, which the GDScript VM will optionally use to run instead of the interpreted code.
  • Integrate some compiler infraestructure like LLVM or QBE to generate this.
External compiler

This is one of the easiest solutions, but it can be quite bothersome for the user to have to do this usability wise. It would make distributing Godot harder if the intention is to make sure code just runs fast. For some platforms like WebAssembly, I am not sure if there is any other way though.

There are tiny and embedded compilers that may be of use, but none really seems fit our needs:

  • TCC: tiny, actively developed and embeddable C compiler that seems to meet all our needs. But license is LGPL.
  • ChibiCC: Seems abandoned.
LLVM / QBE

Another alternative would be to integrate LLVM or QBE into Godot and generate the binary on the fly.
LLVM looks like a lot of work though, since supposedly you have to implement the C ABI code yourself and its codebase is huge. QBE sounds like a much better project for this (it already has full C ABI support) but, while active, its developed by a smaller team and they don't use GitHub.

Lower level binary geneator

There are lower level libraries like SLJIT that do cross-platform codegen. This could work as an AOT compiler for platforms that can do this, and the code generated could be dumped to an shared object in advance on export for the platforms that do not support it (this would need some work).

The downside of this is that these libraries perform no optimization, so GDScript would need its own SSA form optimizer. This may not be terrible, but its some work.

So, what do you think? how would you tackle this problem in a way where the usability and maintainability balance is good in our favor?

If this enhancement will not be used often, can it be worked around with a few lines of script?

N/A

Is there a reason why this should be core and not an add-on in the asset library?

N/A

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

If we can't find a single solution that works, it may be possible we can just do an AOT compiler for platforms that support codegen, and have alternative write to C/GDExtension for platforms that do not.

@vpellen
Copy link

vpellen commented Jan 1, 2023

Disclaimer: My knowledge on compiler infrastructure and build systems is limited.

For me personally, the concern is less that GDScript is slow, and more that the alternatives are miserable to work with. My language of choice for many years was C++, and so GDExtension looks nice enough, but having to download external tools and build systems and thread everything together gets in the way of what I want to do, which is write performant code to solve my problems. I don't feel like I gain any meaningful control from choosing a compiler infrastructure or refining the build system.

I also don't really care about editor integration as much as I care about ease of use. If I could write a single trivial .cpp file and drag and drop it over some random executable that did all the boilerplate garbage and spat out a DLL for me to plug into my project, I wouldn't give a damn about editor integration.

So perhaps a distinction needs to be drawn. Are we trying to:
A) Improve net performance for the average user, or
B) Reduce friction for advanced users trying to accomplish specific ends?

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@vpellen The goal is to improve performance for the average user, so definitely this is not for you.

@AThousandShips
Copy link
Member

I think I'd be in favor of having a good GDScript centric optimizer, working against the codegen format of GDScript, with the option of exporting in executable format via some method. It's a big undertaking but I feel it ensures that GDScript behaves well in all cases (including dynamically created scripts), and makes prototyping easier (not having to export each time to get the optimization but only missing the boost from executable format)

@jabcross
Copy link

jabcross commented Jan 1, 2023

If we go with LLVM, I want to recommend using MLIR. It's somewhat of a more powerful, more generic evolution of LLVM IR that could allow some other things in the future, like compiling GDScript to compute shaders directly.

There's a lot of research at the moment and it's pretty powerful. (It's used by TensorFlow, for instance).

There are already abstractions for most of the common programming language features like structured control flow, functions, etc. And it already has a lot of stuff implemented, like the propagation of debug symbols. So line-by-line debugging would just be a matter of plugging into LLDB.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@jabcross The problem with LLVM IMO is that its enormous and that you are on your own for a lot of stuff to generate IR code. Given GDScript demands a ton of C ABI interoperabilty, C should work better and be simpler than an IR, but of course things are more complicated from there. This is why QBE seems more interesting than LLVM in the fact that its designed around C ABI interop.

I also don't think GDScript has a ton of optimizer potential that LLVM can take advantage of, so another possibility is to do simpler optimization manually and then run on something like SLJIT.

@chamakits
Copy link

(Not a core developer, just a big fan and casual/average user of Godot, mostly through GDScript, dabbled with Mono)
Would this new solution mean to replace the current GDScript implementation, or to exist side by side permanently?

I'm asking because GDScript being a simple included VM is to me one of the biggest advantages of Godot for people to get started. Not requiring a compile step makes iterating on code changes very quick, and having everything included in the editor without any external install makes it the most pleasant Game Engine I've used. I think Godot's GDScript VM is what gives Godot such great ergonomics, at least for average users (like myself :D ).

If this is an alternative to the VM that lives side by side, then I'm a bit less concerned about the ergonomics being impacted. But I'd still be concerned if that increases the workload on the team. It seems it'd be effectively another whole back end (or set of back ends) that is officially supported, and may make future development much more complicated as it requires supporting many back ends. If the team has the bandwidth, then that concern is also addressed.

Do average users run into performance issues frequently enough with GDScript that it merits addressing with this large a shift in implementation strategy? Is there any usage pattern that can be identified that would be better addressed providing some other tooling (for example, nodes that abstract away the concept of resource pooling if a frequent performance concern is creating too many entities)?

@bruvzg
Copy link
Member

bruvzg commented Jan 1, 2023

TCC: tiny, actively developed and embeddable C compiler that seems to meet all our needs. But license is LGPL.

It's x86 and Linux + Windows only.

Cross-building with any external compiler will be a huge pain. Any version of clang should be able to generate code for any target, but linking the final shared library and any use of C/C++ standard libraries won't work without access to the platform SDKs for each target. But I guess the same will be true for the integrated LLVM / QBE as well.

@jabcross
Copy link

jabcross commented Jan 1, 2023

@reduz MLIR addresses a lot of these issues, but you're right that, at least for development of the engine itself, LLVM is quite big. But the compiler could dynamically link to the LLVM library once it's done, so the end users wouldn't be exposed to that.

The GDScript optimizer could be made as a separate module that most engine devs wouldn't have to compile.

MLIR provides a number of ways to deal with C ABI interop. (I had to learn them for my MsC project). There's also a C/C++ MLIR dialect being worked on that will greatly simplify connecting directly to C code at any level of abstraction, so we won't be limited to the ABI forever.

@vpellen
Copy link

vpellen commented Jan 1, 2023

@vpellen The goal is to improve performance for the average user, so definitely this is not for you.

Hey, I'll have you know I'm remarkably average!

Jokes aside, the reason I bring it up is because robust maintainable large-scale integrated performance improvements are probably going to be exceedingly difficult to implement. The average user is not going to be well versed in build systems and compiler infrastructure, which means if you want a good solution it'll have to be integrated with the existing editor in a pretty seamless way.

From what little knowledge I have on the subject, LLVM would probably be the most robust and future proof, but the overhead concerns are real, and I feel like Godot has a history of rejecting existing external codebases in favor of home-grown ones (Bullet is the most recent example of this I can think of). I have a sneaking suspicion that the larger codebases will be deemed excessive, while the smaller ones won't quite fit the needs, which will lead to the development of a custom solution that will take three years to develop and be miserable to maintain.

I'm hoping my concerns are unwarranted.

@Splizard
Copy link

Splizard commented Jan 1, 2023

Something not mentioned yet but I think is worth seriously considering here is using WASM itself as the GDScript intermediary format. There are plenty of fast and actively maintained FOSS WASM runtimes, (AOT, JIT and interpreters).
https://github.com/appcypher/awesome-wasm-runtimes

Wasmer (AOT & MIT), WAVM (JIT & BSD) and wasm3 (interpreter & MIT) stick out to me as being the most performant in their respective categories. They are all close to native (WAVM even claims to exceed native performance in some cases).

Using WASM for this would also enable a better extension experience. No more hassle with making sure that the correct binaries are built and included for all possible platforms ahead of time. Just drop in your one WASM file and use any language that supports a WASM target to write Godot extensions, the WASM runtime will take care of executing the extension efficiently on any of the export targets.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@Splizard WASM is overkill IMO and the memory isolation for something like this really gets in the way. It also does not solve the problems mentioned above (export to platforms that need native AOT).

@badlogic
Copy link

badlogic commented Jan 1, 2023

As someone who actually build a commercial AOT compiler from JVM bytecode to x86 and ARM machine code using LLVM, I think compiling to C is the best option for various reasons:

  • It's way simpler than learning and dealing with LLVM's ever changing APIs.
  • The emitted code is much more easily inspected and debugged.
  • It's easier for contributors to bug fix as the complexity and needed knowledge is a lot lower than contributing to an LLVM based machine code emitter.
  • You can target new architectures/platforms with ease.
  • You still get all the optimizations (mostly) you'd get when emitting machine code directly.

The one downside is that it's harder to emit debugging information, if it's a goal that the AOT compiled code should also be debuggable via the GDScript debugger. I don't think that's a requirement for a V1.

The other downside you mentioned is requiring a C compiler. I don't think this is a problem though. Godot could bundle or on-demand download Clang on all platforms. That's not really much more in download size than bundling the LLVM binaries. If needed, a customized Clang build could be maintained that strip away anything not needed.

@Zireael07
Copy link

Seconding @badlogic in that LLVM is not only big, but also changes A LOT.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@badlogic I think the AOT route via C is useful, but its still more hassle than what Godot users are accostumed to. IMO this is something that we should have to support anyway at some point (else Wasm, iOS or console optimization will not happen) but, as you say, for development this is a hassle.

To me C AOT (basically compiling GDScript to a C code that uses the GDExtension API) is something that is very simple to do (likely few thousands loc) and maintain.

But for development, my feeling is that we should have something that works out of the box, which is why I still think something like SLJIT makes more sense. The only thing you miss is the optimizer pass, but given the nature of GDScript I doubt this will be much a problem and the performance gain will still be very significant.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

For reference, either as LLVM, C or SLJIT, all that needs to be done is provide a VM back-end:
https://github.com/godotengine/godot/blob/master/modules/gdscript/gdscript_vm.cpp
That builds its data from the generator:
https://github.com/godotengine/godot/blob/master/modules/gdscript/gdscript_byte_codegen.h

Either spitting C or building an SLJIT execution buffer based on this is not a lot of work (order of thousands of lines of code). The reason is that most of those instructions, if you look closely, already call C functions to do most of the heavy loading (as an example, all built-in types, operators, constructors, and all the entire engine API are available via C function pointers). Eventually, significant optimization can be achieved by providing specialized versions of the instructions when the types are known (the codegen.h already lets you know this, so this work is already done). Even adding an SSA optimizer with most common optimizations would likely not be very hard.

In contrast, writing an WASM, LLVM or QBE back-end would probably be orders of magnitude more complex, because you need to replace all this with inline code in IR format. Of course, this has more optimization potential than the previous methods, but the amount of work required and the maintenance requirements are enormous.

@badlogic
Copy link

badlogic commented Jan 1, 2023

@reduz I don't think a C generator + compiler requirement is a hassle. Users already have to download export templates for all platforms. Providing the user a single click download inside the editor to fetch (a trimmed down) Clang seems to be pretty OK to me in terms of hassle.

As you said, you will need an AOT solution for iOS and WASM anyways, where SLJIT won't work. It's also questionable if an SLJIT generator can actually achieve any performance improvement compared to the current bytecode interpreter.

@Splizard
Copy link

Splizard commented Jan 1, 2023

@reduz

It also does not solve the problems mentioned above (export to platforms that need native AOT).

WASM runtimes totally support native AOT.

Wasmer 1.0 introduces “wasmer compile”, a new command for precompiling Wasm files. Our customers and developers use “wasmer compile --native” to precompile WebAssembly modules into native object files like .so, .dylib, and .dll.

(https://medium.com/wasmer/wasmer-1-0-3f86ca18c043)

the memory isolation for something like this really gets in the way

Reference types in WASM are a thing (also supported by wasmer), I'm not sure about the performance profile of say keeping all packed arrays and strings as reference types but it's certainly a solution.
Modding is certainly an area where WASM runtime isolation could be helpful.

As @vpellen indicates, I don't think you can beat the user experience of dropping a single WASM file into your project and being able to export this seamlessly to mobile devices, consoles, etc. Let the WASM runtimes focus on performance!

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@badlogic It is a hassle if this is a requirement to even use the engine. Godot is a tiny download that just works and a large amount of the Godot community largely appreciates this. The export templates are only downloaded at the time of exporting the game, but you don't need them to start working quickly.

I agree with you that this is likely the path we will have to take at some point, but it should only be an optional step for games that only really need this extra performance. The current bytecode interpreter still has a good amount of room for optimization (addressing needs to be further simplified and typed instructions need to be added), but as the intepreter is a gigantic jump table, it still has a big toll on the L1 caches and branch predictor, which should be significantly improved with something like SLJIT, and that works out of the box with little work.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@Splizard Its good to know you can handle AOT but, as I said, I still think the isolation sucks and creates a lot of glue friction that I would be happy to avoid. Additionally, you still need some sort of compiler so, in the end, the advantage provided is minimal vs the drawbacks.

@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 1, 2023

For the reference, there is an existing proposal for a WASM runtime with a decent amount of support, but not much discussion: #3370

@xsellier
Copy link

xsellier commented Jan 1, 2023

I know it is an unpopular opinion, what about dropping GDScript and using Python instead?

I would like to clarify my point. As a matter of fact, writing a programming language is the work of a full-time team. I find it ambitious for Godot Engine to create a game engine with a whole new approach with a new programming language.

Perhaps Godot Engine should do with GDScript the same thing that happened with VisualScript. At the same time switching to another language with a syntax close to GDScript, such as python. It should close a lot of tickets directly related to GDScript and allow us to focus on the core of Godot Engine, i.e. being a game engine.

I'm aware that this is only my opinion, and that I'm not the person who maintains GDScript, nor the one who will be in charge of switching to Python, and even less the one who maintains Godot Engine in a broader sense.

@YuriSizov
Copy link
Contributor

@xsellier
Juan just recently recalled the creation of GDScript on Twitter, and mentioned why Python was not a good option. It is also covered in part in our official documentation.

We like GDScript because it allows us to tightly integrate into the engine, to remove a lot of overhead both conceptually and in terms of the interop. Maintenance of our own tool does come at a cost, but so does maintenance of a blackbox solution that we must embed into our project. Take our approach to physics for example, you see the same thing. We opted to abandon Bullet as the main backend because it is very hard to keep that bridge between the two parties sound. Same applies here.

Aye, it will close some bugs in our codebase. But it will also open new bugs which may not be within our reach to fix. So we'll have to bother other projects with our issues, projects which may not share our goals or don't see our problems as worthwhile. But our responsibilities before our own users won't go anywhere, so we'd have to either take the blame or ship some hacks and ad-hoc fixes, if we can. That's not great at all.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@xsellier Python is a terribe option. Letting aside that there is not a good VM that we can use and that all the nice integration to the editor would be lost, the VM is not designed to work standalone. Even if you make it work standalone, you lose access to the rest of the ecosystem, or you bloat massively your exports as a result.

Python is designed so applications are parts of its ecosystem and not the opposite. It definitely is nice to have Python support for some things (if you work on scientific or academic fields as an example) but it's not meant to be used for something like Godot.

@xsellier
Copy link

xsellier commented Jan 1, 2023

I get your point, Python might not be the best choice because of the size of its VM.

My point is still relevant, writing a programming language is a full-time job for a whole team of dev. Meaning, either hiring more devs to work on GDScript, or dropping it and take another already existing programming language.

I'm quite happy about what's GDScript is becoming, First class functions, static methods, new built-in types... but for the last 2 years, progress reports about GDScript are quite sparse ( https://duckduckgo.com/?sites=godotengine.org%2Farticle&q=GDScript&ia=web ). I mean I'd expect more update on GDScript since it is a programming language with quite some bugs ( https://github.com/godotengine/godot/issues?q=is%3Aopen+is%3Aissue+label%3Atopic%3Agdscript ). Some bugs are more critical than others and lasts for years ( godotengine/godot#7038 ). I'd like to state it is not a complain, because I use GDScript and I like the way it is, it is more like a realistic statement.

My point is more, creating a programming language is too much work for Godot Engine atm, it might be better to think about dropping it, to focus on what's imprtant for the engine. You can focus on Python if you want, I was talking about dropping GDScript in favor of something else.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@YuriSizov Its probably not a lot of work to have a Wasmer module that can load GDExtensions inside the runtime compiled for Webassembly. There is likely not a lot of merit either, but using it for running GDScript is definitely not a good solution. GDScript uses engine types for everything, so the isolation means the glue needs to either spend a lot of time converting the engine types to communicate with the engine, or it needs to keep types as external references and access to them is slower.

This is what happens in C# as an example, where you need to be conscious on using engine types or native types depending on what you need for the most part (accessing data or passing it), which is fine for this language as users are expected to be more experienced. But for something easy to use like GDScript that is meant to "just work", it will cause a lot of friction.

@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 1, 2023

My point is more, creating a programming language is too much work for Godot Engine atm, it might better to think about dropping it, to focus on what's imprtant for the engine. You can focus on Python if you want, I was talking about dropping GDScript in favor of something else.

@xsellier
Besides a couple of links to address your specific proposal at the start the rest of my comment was not about Python, it was exactly about the cost of dropping a bespoke solution which is GDScript for something else. You keep implying that using an off-the-shelf language would somehow be free, or at least cheaper to maintain. I made a point that it's not the case.

@reduz
Copy link
Member Author

reduz commented Jan 1, 2023

@xsellier The problem with your logic is that, what I believe you fail to understand (likely due to lack of experience in the field) is that Integrating another programming language to Godot and keeping the fantastic "just works" usability GDScript is even more work than developing GDScript.

Writing glue code is difficult and there are a lot of corner cases:

  • How do you handle type conversions to engine conversions efficiently?
  • How do you handle threads?
  • How do you handle a class model that resembles and references the same as the engine?
  • How do you handle exporting and packing?
  • How do you handle reference assignment and garbage collector with engine types?
  • Etc.

I already integrated other languages such as Lua, Squirrel and Python to Godot. It was a ton of work and generally there is more code to do the glue than there is GDScript runtime code, and inefficiencies everywhere when getting data one way or the other (same reason why I think Wasm as script runtime is a terrible idea). This without even getting into all the nice editor integrations GDScript has to the engine, which would be a massive amount of work to do in another language (where in GDScript they are tiny).

Building software is not stacking lego bricks where you add something existing and it magically works. Getting the pieces to fit and keeping them together often requires as much effort as writing the pieces yourself.

@Qws

This comment was marked as off-topic.

@Qws
Copy link

Qws commented Apr 14, 2024

I feel like some of the godot users don't understand the power of GDExtension. If you want to run fast (C++ fast) just use that. Why GDScript needs to compete with other languages in terms of speed?

This is my approach of using Godot:

  • Use GDExtension for API (Backend)
  • Extend the functionalities in GDScript (Frontend)

I think that simply just improving the VM is the best and "easiest" way of going forward with GDScript. Some of the users complain about the game shipping with the interpreter and parser for the GDScript, but how big is that exactly? I don't think there is a significance amount. In future maybe there could be an intermediary representation for the language in order to make the game ship with a simpler parser and smaller interpreter? Right now there are tons of optimization that can be made for the VM so there is room for improvements.

Indeed, speed is not the concern for most GDscript users I think, but I do think the fact that Godot compiles GDscript to plain text is an issue... which allows others with bad intention to open the game in Godot Engine as if the project is fully open source.

C# does not fix this issue, and C++ is super complex.

I wish Godot supported Swift instead of C#, as Miguel states, C# and its GC is a billion dollar mistake in gaming industry.

@Lcbx
Copy link

Lcbx commented Apr 14, 2024

You are probably saying this after watching Miguel de Icaza's video or something related.

C# support was a political move iirc ; so Unity users would migrate to Godot. I believe it has had good results on that front.
As a language, it is also a decent tradeoff between easy scripting and performance, and the GC is not that bad nowadays.

Swift is more of a c++ equivalent that is more developer-friendly, and has a future for making GDextension modules :)

@kisg
Copy link

kisg commented Apr 14, 2024

@Qws

This is a bit offtopic here. If you are interested in Swift support, please open another proposal or start a discussion on another forum.

However, Swift is already supported today with SwiftGodot: https://github.com/migueldeicaza/SwiftGodot

You can even embed Godot into a SwiftUI app with it using the libgodot PR (now under review): https://github.com/migeran/libgodot_project

@SlugFiller
Copy link

You all do realize SwiftGodot already exists, right? C# currently exists as an optional module, but there are talks about porting it to a GDExtension instead, so it would be on identical footing to SwiftGodot. This is considered to be an improvement to the C# integration.

Mind you that there's a hidden disadvantage to using Swift over C#: C# compiles to CLI, which is in turn executed by the runtime available on each target platform (Mono, .Net Core). Only a single CLI build is necessary, since CLI is an intermediate language.
By contrast, Swift requires a per-target build. Cross compilation is coming around Swift 5.11, so currently, such a build requires actually having each of the target platforms (that means owning both a PC and Mac). Even Swift's closest with to an IR target, WebAssembly, is only available on Mac host at the moment.

Basically, it boils down to: Swift is still too new, and cross-platform support is still incomplete.

@petar-dambovaliev

This comment was marked as off-topic.

@donte5405
Copy link

donte5405 commented Aug 21, 2024

As a quick reminder, Godot 4.x HTML5 export is no longer possible to be published to Cloudflare due to its huge size (godotengine/godot#70672 godotengine/godot#68647).

TLDR; If any machine-code-related approaches will exist, I prefer external tool approach, as it's what other game engines have been doing and it doesn't conflict with Godot's design approach (as to be portable, self-contained, and has great backwards compatibility).


External GDScript optimisation tool sounds like the most logical way to implement into Godot, as it's what other commercial tools have been doing for years. While having all-integrated tool that users can just download and use in single binary blob (Godot's design goal) is nice, but its size difference is too significant to let go especially if such approach is going to be used in platforms with specific requirements or very limited resources such as Apple iOS or HTML5.

For comparison, GameMaker specifically requires its users to install platform-specific SDKs and at times also requires its users to have another machine being set up as a compiler machine instead of developing an AIO integration like what Unity has achieved with its IL2CPP solution, to workaround huge development time. While it sounds daunting, it isn't all that difficult if guided properly since the rest of the process, the tool will take the task and do rest of things for users already. There isn't really anything lower-level that users must have the knowledge in order to utilise such tools. Even better for Godot because it already has much better integration than GameMaker that still relies heavily on third party toolchains most of time.

GDScript/C++ transpiler doesn't sound too foreign as it's what it was done previously. It's just that nobody really has any interests bringing it to the mainstream or improve it to become user-friendly enough (especially the proper GUI, I yet to have seen any tools with a proper user interface, including my own tools, which I planned to have them later). It's still huge work (especially proper debugging functionality) but at very least it's a lot less than other LLVM or all-integrated approaches that also introduce side effects and/or make existing problems even worse (notably, editor/export size and it breaking compatibility with some platforms).

The traditional GDScript binary interpretation should always exist. I personally don't ever think that GDScript should become a full-fledged or drop-in programming language. It already works well enough with tasks it's designed for (as to be a high level scripting language), and even better with the recent optimisation patch, but it still should never be treated as a programming language that will perform immense amount of complex calculation tasks at any given seconds. Those, if possible, should be implemented in more performant languages either in the core if it's significant enough for majority of users or custom builds/GDExtension if it's task-specific. Even if ease of use is a priority, it should always be a task for talented community to make those solutions available and can be applied easily for non-tech-savvy users. Imagine AIO chunk generator, or MMD model loader/interpreter, or something that's rather small but has huge impact but only for server builds such as Argon2 hashing module. I'm honestly surprised that there yet to be any GDExtension related asset stores for Godot yet.

@Sciumo
Copy link

Sciumo commented Aug 21, 2024

Something like this?: GdScript2All

@7f8ddd
Copy link

7f8ddd commented Sep 13, 2024

Might be irrelevant, but Roblox's Luau fork has code generation.
https://create.roblox.com/docs/luau/native-code-gen
https://github.com/luau-lang/luau/tree/master/CodeGen/src

It also doesn't use LLVM and works on all platforms because... Roblox.

@atirut-w
Copy link

It also doesn't use LLVM and works on all platforms because... Roblox.

Nah, thank their server using one platform for that.

server-side scripts in your experience can be compiled directly into the machine code instructions that CPUs execute

@7f8ddd
Copy link

7f8ddd commented Sep 13, 2024

It also doesn't use LLVM and works on all platforms because... Roblox.

Nah, thank their server using one platform for that.

server-side scripts in your experience can be compiled directly into the machine code instructions that CPUs execute

I just meant the concept in general, although it might have some issues I can't think of.

It appears to me that they made it in a way where they can add new platforms by simply adding new builders, eg. AssemblyBuilderA64, AssemblyBuilderX64, etc., and it takes a compiled function from the VM's stack at some index, and converts it into native code using one of the builders.

Oh, and I just want to note that it's definitely intended to be used on clients at some point, because otherwise it would be pretty pointless to be on Roblox's servers where the architecture is always the same. I think that's their goal in the future based on one of their forum posts.

@the-ssd
Copy link

the-ssd commented Dec 9, 2024

Hello everyone,

I have made a prototype JIT compiler for GDScript.

On this example:

func fib(iter: int) -> int:
	var n1 := 0
	var n2 := 1
	for i in range(0, iter):
		var n := n2
		n2 = n2 + n1
		n1 = n

	return n2

My JIT is around 60-70% faster. And it can call functions directly, but I haven't tested it with function pointers to the engine.

I am using cranelift for codegen, which is written in rust, so likely won't be used in Godot. Cranelift is also better suited than LLVM for JIT compilation, because it takes fewer steps when producing machine code, while being only ~14% slower.

I will continue working on it, and maybe at some point it will be good enough for use in production.

I also would like to add something to the discussion:

  1. JIT compilers can be made to emit Object files. But you will need to use a jump table, which can cost a bit of performance. (Only benchmarking can tell if is it slower)

  2. WASM probably won't be that good. Because GDScript makes a lot of calls to the engine, every time a function is called, it will have to first cross the WASM boundary. In my testing, this is a noticeable amount of overhead. Which is why I compile to machine code.

  3. @Qws Swift uses reference counting, which isn't significantly better than GC.

@Journeyman1337
Copy link

Journeyman1337 commented Dec 12, 2024

LLVM is very easy to use. I made a new language with it in under a month when having no experience. If you guys already went through the effort of making a bytecode, using LLVM should be a piece of cake. You could generate LLVM IR straight from your bytecode format. As for linking with your C++ codebase... you can support the standard Itanium ABI scheme and compile using Clang to make sure its used as you expect. LLVM has a tool for determining mangled names in Itanium: https://llvm.org/docs/CommandGuide/llvm-cxxmap.html.

@the-ssd
Copy link

the-ssd commented Dec 12, 2024

#6031 (comment)

I actually agree. I also started with zero knowledge of making compilers (and writing rust). And was able to make a prototype GDScript compiler. But I hit a roadblock, which was GDExtensions (wasn't able to find any documentation).

Currently, LLVM seems (to me) like the best solution for Godot.

Still, I would argue that a JIT, would be faster, since with JIT compilers you don't need PIC code, which makes it faster to call functions. And really most of the things GDScript does it calling functions.

Edit: If compiled code will be statically linked, then this doesn't apply. But I am not sure how this would work

I was able to only find this benchmark, which says that when calling via function pointer, -fPIC causes a 0 to 30% performance hit. And it's not a problem as long as they're not used in the innermost loops
NOTE: this is benchmarking call to a library, and not from it, but the same logic should still apply.
I would argue that this is leaving performance on the table. But currently, GDExtensions also has to go through this.

LLVM has a JIT, but LLVM takes it's time to compile, which can be a problem during startup, though this can be worked around with first using an interpreter then switching to compiled code, which adds complexity and makes it harder to maintain.

Cranelift would be perfect for this, as it generates fast code quickly. If it wasn't as low level (btw, I am working on fixing this) and had C bindings.

@SlashScreen
Copy link

I disagree with the idea of native compilation being the solution for GDScript, at least at first. I think a great amount of work can be done with the VM as it is to improve performance. Here's my issues with native compilation:

  1. Native compilation can produce lots of issues on our target platforms, particularly web. This has been an issue for a while with GDExtension.
  2. LLVM and other backends can be slow. Godot is fast and light, and we should keep it that way, particularly with scripting languages allowing for faster iteration times. There's a reason Go uses its own in-house solution, and Zig is moving away from LLVM.
  3. Introducing LLVM or another backend significantly increases complexity and line count, and with more complexity comes more burden on the mostly-volunteer maintainer base.

This isn't to say I disagree with compiled languages - in fact, I prefer them - but GDScript is a scripting language and we should play to its strengths as a run-anywhere embedded platform. We need look no further for examples of very fast interpreted languages than Lua (although I recognize LuaJIT) or Wren. I think we can get competitive with them without static compilation. Here's what I propose:

  1. First, focus on a code optimization pass. There's a few optimizations we can knock out without much issue, like Dead Code Elimination via branches and useless code, which the LSP can already detect (Side note: is SSA IR on the table?), constant substitution, compile-time expression evaluation (Like pre-computing var x = 3 + 1), and loop unrolling. (Also, something needs to be done about the if Engine.is_editor_hint(): thing. That really bugs me.)
  2. Next, we bring in the promised typed code improvements. I'm not actually sure what this would look like beyond turning off type checking. I imagine there could be some other things, like bit shifting integer division, and the like.
  3. This may be controversial, but I greatly disagree with the current VM design. I think it should be a register machine with a greatly simplified instruction set (Seriously, why does the current VM have over a hundred opcodes?) There are a lot of different answers to why, but register machines tend to be faster, but they do. Also, this could open the door for later optimizations with out-of-order execution, which in a lot of cases is easier with a register machine. (Another side note: A new VM should also be able to be sandboxed for modding capabilities.)

I recognize a lot of this already increases complexity, but I would argue this is less of an ask than including LLVM, either internally or externally. Also, the less external dependencies, the better, particularly on platforms like Android.

As for the JIT question - technically, that falls under the native code problems, since it executes platform-specific machine code. On the other hand, it is without a doubt a huge speed boost and valuable for games. I am on the fence about this. If we do go this route, it should be a tracing compiler, since so much of game flow runs on loops.

I would love to contribute to GDScript, and if I should start poking away at this in my spare time, just give me the go-ahead.

@the-ssd
Copy link

the-ssd commented Dec 24, 2024

  1. It is possible to add support for Web, but that will have to wait, until that a VM can be used.

  2. LLVM can be made optional, so it shouldn't add a lot to the size, and for fast iteration the VM can be used.

  3. Using LLVM isn't significantly harder than making a Byte code interpreter. Here is how you can get started

  4. Making a tracing JIT instead of a static JIT seams like just more complexity without a point. Assuming static types, it shouldn't be faster. And it might even be slower because of warm up time.

@YYF233333
Copy link

@SlashScreen Nice to hear you want to do some work in gdscript. I happen to be reading gdscript modules, here are some points which may help:

Side note: is SSA IR on the table?

As far as I see, GDScript compiler is a one-pass compiler, which directly gen final form code from AST walking, if you are looking for sometype of IR you might be disappointed. This might be due to the need to compile code during game loading. And as long as we still need to compile on loading, we are facing resistance in doing complex optimizations.

There's a few optimizations we can knock out without much issue, like Dead Code Elimination via branches and useless code, which the LSP can already detect

These logic only present in debug build (editor/template_debug), some work should be done if you want to reuse them in template_release.

Next, we bring in the promised typed code improvements.

Actually we seem to lose performance for static typing, take Array as an example:

Typed Array Assign
OPCODE(OPCODE_ASSIGN_TYPED_ARRAY) {
CHECK_SPACE(6);
GET_VARIANT_PTR(dst, 0);
GET_VARIANT_PTR(src, 1);

GET_VARIANT_PTR(script_type, 2);
Variant::Type builtin_type = (Variant::Type)_code_ptr[ip + 4];
int native_type_idx = _code_ptr[ip + 5];
GD_ERR_BREAK(native_type_idx < 0 || native_type_idx >= _global_names_count);
const StringName native_type = _global_names_ptr[native_type_idx];

if (src->get_type() != Variant::ARRAY) {
	OPCODE_BREAK;
}

Array *array = VariantInternal::get_array(src);

if (array->get_typed_builtin() != ((uint32_t)builtin_type)
|| array->get_typed_class_name() != native_type
|| array->get_typed_script() != *script_type) {
	OPCODE_BREAK;
}

*dst = *src;

ip += 6;
}
DISPATCH_OPCODE;
Untyped Assign
OPCODE(OPCODE_ASSIGN) {
    CHECK_SPACE(3);
    GET_VARIANT_PTR(dst, 0);
    GET_VARIANT_PTR(src, 1);

    *dst = *src;

    ip += 3;
}
DISPATCH_OPCODE;

The type is checked at runtime and we still fall into the same path as untyped assign. I guess nothing has been done yet to speedup typed code.

I think it should be a register machine with a greatly simplified instruction set (Seriously, why does the current VM have over a hundred opcodes?)

I have no preference between stack and register based, but I support the idea of simplifying instruction set (even at the cost of some performance), a 3500 LOC function is definitely not something easy to deal with. Also the jump threading implementation could be reevaluated. Based on this paper, it may not providing enough performance gain to make up the maintainability cost.

@SlashScreen
Copy link

LLVM can be made optional, so it shouldn't add a lot to the size, and for fast iteration the VM can be used.

You're right, and I'm not arguing against the idea as a whole, despite what it may seem; I simply believe our efforts should be focused elsewhere in fixing the systemic issues of the language rather than slapping an LLVM bandaid on it. It should be optional (even a GDExtension) and not be default.

Making a tracing JIT instead of a static JIT seams like just more complexity without a point.

You're right about this, actually.

if you are looking for sometype of IR you might be disappointed.

I figured this was the case. SSA might be doable in the AST stage, but I doubt it would be worth the trouble.

And as long as we still need to compile on loading, we are facing resistance in doing complex optimizations.

Perhaps I misunderstood what AOT means in this context; If it actually means compiling into bytecode on project export, I am actually for the idea. Although, for game mods and other things, there should retain the ability to do runtime compilation, and/or the ability to load bytecode directly. I'm worried, though, that it could become like C# if we take this too far (that is to say, cumbersome and kind of annoying).

some work should be done if you want to reuse them in template_release.

That I understand; what I meant is that a lot of the logic is present in the LSP, and just needs to be copied and pasted and then modified to suit the environment.

a 3500 LOC function is definitely not something easy to deal with.

Absolutely. The first changes I would probably make are

  1. Condensing all the opcodes for each primitive type (for example, OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY and friends) into one opcode that has an integer argument for which type it is. Once types are unified in the engine itself (a separate issue that needs attention), perhaps the native/gdscript type distinction can be removed entirely.
  2. Extract each opcode operation into a function, and then create a table of function pointers to them with the opcode as an index.

The register machine idea is attractive, but would also require a redesign on the entire VM and significant compiler adjustments. But, y'know, is that a bad thing?

@jabcross
Copy link

Idea that occurred to me.

Have we considered just adding the option of running GDScript on top of the .NET VM? We already ship the runtime with the C# version and we already have all the plumbing with regards to the bindings. Seems to me like the least-effort way to get immediate performance.

I don't think it's realistic to maintain something with better performance than .NET. I think there are a LOT of developers that would benefit from the .NET VM without having to learn C#, and developers that actually hit the garbage collection issues should be using a language with explicit memory semantics, which GDScript isn't.

Maybe we shouldn't be trying to make GDScript into the N+1th compiled language.

@SlashScreen
Copy link

That isn't a bad idea, actually. A few comments, though:

  1. The C# people are looking to move C# to a GDExtension instead of it being builtin, which would remove that runtime from use.
  2. Again, solution files are kinda annoying and take a little while to build.

I do absolutely agree about not making GDScript the umpteenth compiled language, especially since it wasn't designed as such and so trying to force it to become something it was never meant to be would be awkward.
There's another route we could go down though: Multi-target support, like Haxe being able to compile into a bunch of runtimes. Might be more effort than it's worth, but it's something worth considering.

@Lcbx
Copy link

Lcbx commented Dec 25, 2024

The C# people are looking to move C# to a GDExtension instead of it being builtin, which would remove that runtime from use.

the .Net runtime is not shipped with the version of Godot used by most users,
That's why there are 2 different downloads/builds on the godot webpage.

Moving it to a GDExtension means that users just download Godot and install the C# addon when needed ;
that's a better UX I believe.

@Lcbx
Copy link

Lcbx commented Dec 25, 2024

Have we considered just adding the option of running GDScript on top of the .NET VM?

If we do make GDScript run on the .net VM (called CLR), it would probably be with the goal to move to it definitively.

Potential benefits and drawbacks of moving Gdscript to the CLR :

  • by unifying the C# and GDscript runtime there would be less maintainance for both options,
  • C# support would be 'free' (and stuff like Expose a zero allocation API to Godot C# bindings #7842 become very important)
  • Otoh GDscript would inherit .Net's Garbage Collector (you use the VM, it comes with the GC, no way around that)
  • GDscript hot reload might become slower if based on the current .net runtime integration

@heavymetalmixer
Copy link

heavymetalmixer commented Dec 25, 2024

I'd rather have GDScript not depend on .NET at all. LLVM would be a good idea for CPP, but make Godot tell the user to download it from a certain link and a guide to set it up.

@jabcross
Copy link

jabcross commented Dec 26, 2024

I'd rather have GDScript not depend on .NET at all. LLVM would be a good idea for CPP, but make Godot tell the user to download it from a certain link and a guide to set it up.

It wouldn't be a dependency, it would be an optional backend, just as LLVM would. And we could also link dynamically to a system .NET installation too, if the concern is bloating the binary.

Both should be plugins, though.

@SlashScreen
Copy link

SlashScreen commented Dec 26, 2024 via email

@SlashScreen
Copy link

If you guys want to work on alternative backends for GDScript, you can, but I don't have much interest in doing it myself. What I'm going to do next is attempt to reduce the instruction set and refactor the main eval function to be easier to work with in a PR. I think that's an achievable first step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests