Optimizing Jint part 5: Optimizing Javascript primitives
Javascript primitives in Jint
Jint has abstraction for each primitive Javascript type. Having these fundamental types work and interact with the engine in efficient manner is crucial for good performance.
In this blog post we are investigating some of them and problems I found while profiling.
In this blog post we are investigating some of them and problems I found while profiling.
Caching common values
Javascript primitive types in Jint derive from JsValue so a types like JsString and JsNumber share the same base class, JsValue.
When we are discussing primitive values like JsString or JsNumber, we can rely on the fact that they are immutable, just like commonly in programming languages, like C#. They cannot be mutated and thus JsNumber(1) == JsNumber(1). This leads us to the revelation that common values like numbers 1-1000 should be cached as static inside the library as well as char values (JsString with length of one) which are quite finite set. Also converting from string to integer and the other way around is quite common in any execution environment.
You can see the change and performance number in these pull requests:
Here's the factory JsNumber.Create, which handles detecting common values. Did you know that Javascript differentiates between negative and positive zero? It almost seems that when building the specification, they deliberately chose poor performing alternatives like for Map and Set, but more about those later...
- Cache common JsValue allocations - reduce memory allocations by 10% just by making sure that hot paths don't allocate unnecessarily
- Cache common int to string conversions - 30% of memory allocations gone by making sure common values are available in cache
It's important to note that many applications have usually quite fixed set of values they operate with. It pays off to recognize these common patterns, ensure their immutability and cache them when possible. Integer and char values here were especially rewarding as they map to array indexes and don't require hash-based lookups. We also save some memory by detecting when Javascript float is a whole number and maps to same integer value, thus sharing the same JsNumber instance.
How to make fast lookups
Sometimes checking alternative execution models with regards to branching can also reveal a better performing alternative. See faster code paths for JsString.Create. Here both BenchmarkDotNet and SharpLab can help. See the changed IL code, 20% vanished by changing used constructs. This is where we enter the micro-optimization area, but which can benefit a lot when code is on hot path.Here's the factory JsNumber.Create, which handles detecting common values. Did you know that Javascript differentiates between negative and positive zero? It almost seems that when building the specification, they deliberately chose poor performing alternatives like for Map and Set, but more about those later...
The dreaded allocating equality operator
Usually a moderately trained eye can see performance pitfalls in code and algorithms quite easily, but this time the code seemed fine - but the profiler told otherwise.Here's an example of such pattern that was allocating:
This example was elaborate on purpose. It's just regular JsString representing a JavaScript string and there's a check that it shouldn't be null and it's internal field value of type string should not be null and have some length.
So why does it allocate? The culprit was operator overloading. See how JsString and many other primitive types overloaded equality operator for convenience, but also caused a lot of unnecessary allocations when interpreting the script.
When you thought you were just doing simple null reference check, you were actually dispatching a method and doing multiple branches. And the fix? In every single place, we needed to use ReferenceEquals(myObj, null) instead of == operator. Another nice option with the new c# 7 language featur, pattern matching , is to write myObj is null. Here's a good blog post about the ways to use pattern matching and it's performance characteristics.
Comments
Post a Comment