Offloading code to C++ in a Babylon Native project
One benefits of using Babylon Native to run your Babylon.js scripts in a native application is the possibility to have a single codebase across your web and native projects. However, one other major benefit is to be able to offload computationally intensive tasks from the JS to native very seamlessly. This can be a powerful tool in some specific scenarios (such as when JIT is not available), but it might also not bring that many benefits as most people would think. In the blog I will try to provide examples on how do to that and also talk about some corner cases where results might be very unintuitive.
Two Babylon Native application running the Doom Fire algorithm, one doing calculations in JavaScript and the other in C++.
Choosing a computational intensive algorithm
For this particular demo we needed a algorithm that would be simple to understand and modify but it would also be computational intensive enough to have an impact in our frame time. I have chose the DOOM fire algorithm because it is simple to understand and can give us cool visual results. This algorithm basically take intensity values and update them very frame based on the values of the pixels below minus a random value.
Doom fire algorithm.
In order for use to have a big number of things to apply the propagation to, I’ve created a cube of 80 x 80 x 80 Babylon.js Thin instances objects. We will use the Doom Fire algorithm to set the color value of each of those instances very frame.
All calculations are done using integer values for fire “intensity”, those values are later converted into color by using a table similar to the one below.
Color table for colors based on fire intensity.
The algorithm has basically 3 steps, first we set the fire at the base of the cube, then we propagate the fire up using a random number for decay and finally we convert the fire intensity into color using the color table. The rendering loop in the Babylon.js scene looks like the following:
The main part of the algorithm is the fire propagation logic, even though it is fairly simple it must run for every single instance in our scene. The goal for our comparison is to have to very similar implementation of the algorithm, one done purely in JS and the other done using C++.
C++ implementation:
JavaScript implementation:
The goal was to keep those two implementation as similar to each other as possible to allow for a fair comparison. It would not make much sense to compare the performance if both runtimes are using completely different implementations. However, there are many possible performance optimizations that could be done in either C++ and JavaScript.
Sending Data from JS to C++
To be able to send data from JS to C++ we must first register a C++ callback function in the JavaScript global scope. To do this we use the Babylon::AppRuntime class from the Babylon Native API to dispatch code to be executed by a JavaScript thread. This will also give us access to the Napi::Env object that basically holds the state of our JavaScript runtime.
Using the Napi::Env we can get access to the global scope of the JavaScript runtime and set the ‘nativeUpdate’ name to be bound to a function object. This function object will callback into C++ when executed by JavaScript.
Calling this code from JS is really simple and it behaves exactly like a standard JavaScript function.
There are other ways to send data from JS into C++. It is possible to create classes and complex objects that can be marshaled back and forth. However, using simple data structures such as integers, floats or Arrays is by far the most performant way, since the JavaScript runtime will do very little manipulation with the objects.
Problem 1 — The random number generator problem
When doing the first tests for the blog I noticed that the pure JavaScript version of this demo was running faster than the C++ counterpart!
This shows us not only how well optimized modern JavaScript engines are but also that moving heavy logic to C++ might not always lead you to better results as most people might think. Sometimes performance problems can be very tricky to track down.
After doing some investigation on the code and what could cause JavaScript to perform so well compared to C++ I was able to narrow it down to one thing: The STL random number generator vs Math.random().
The code of the fire algorithm is to update the fire propagation based on a random number. We also use random number to determine the wind contribution and the base fire intensity. Therefore, this code calls Math.random multiple times. It seems like Math.random in JavaScript is a lot faster than the random number generators provided by the C++ STL library.
Random number generator are a very complex and deep topic and can be a science on its own. There are multiple tradeoffs when choosing a random number generation algorithm and sometimes the quality of the random distribution can compromise speed. It is very possible that the C++ STL have chosen a more precise but slower algorithm compared to the one used by JavaScript, but I haven’t done further investigations on this to know the exact reason.
To account for that, I took the implementation of a RandomNumberGenerator class from DirectX 12 Mini engine project that allows you to choose if you want to use a STL version or a custom “faster” algorithm for the random number. By setting the USE_STL define in Random.h you can switch between the two approaches and see how that impacts the overall performance.
One important lesson to take from this is that performance usually comes with tradeoffs, it is not because something is implemented using language A that it is going to faster or slower than language B. Modern compilers and interpreters are very smart and can optimize your code in non-obvious ways. So the first thing about is to always measure, and don’t just assume things are fast or slow.
Problem 2 -JIT vs no JIT
Modern JavaScript engine have a technology called Just In Time Compilation (JIT). This technology allows JavaScript to be compiled to platform specific assembly code during first execution and then be reused during code execution. This can vastly improve the performance of JavaScript since this process only happens once and after that the code will have performance similar to native code.
Knowing how to make your code JIT properly is one the most important performance optimizations web developers can do to speed up their code.
However, some platforms do not allow JIT (mainly iOS). This is very important to be considered since JavaScript will probably run much slower that way.
Demo running with no JIT but computation happens in JavaScript.
For these cases it is even more important to consider moving heavy tasks to C++ since they will not be affected by the fact that iOS does not support JIT.
Demo running with no JIT but computation happens in C++.
Final considerations
Hopefully this blog was able to show a very simple example on how C++ and JavaScript can work together in a Babylon Native project to deliver the best performance to users in each scenario. Even though this example was simple I think I was able to show how performance is not always obvious and require careful consideration about technologies and approaches.
The project can be found at the following Github repo: SergioRZMasson/BabylonNativeDoomFireDemo: Demo showing cases where computation can be formed by JS vs C++ in a Babylon Native project. (github.com)
Also, for those who want to view it on the web, I’ve also created a Babylon.js Playground: DOOM Fire 3D | Babylon.js Playground (babylonjs.com)
References
Used C++ Random number generator: DirectX-Graphics-Samples/MiniEngine/Core/Math/Random.h at master · microsoft/DirectX-Graphics-Samples (github.com)
Doom Fire algorithm: filipedeschamps/doom-fire-algorithm: Playground for the fire effect from DOOM. Really simple algorithm and all experiments are welcome! (github.com)
Sergio R. Z. Masson — Babylon.js Team