How polymorphic JavaScript functions affect performance
As with any conversation about performance, we need to gain some shared context around the type of JavaScript code we want to optimize and the context in which it will run. So, let’s start with some definitions:
Performance. First of all, when we use the word performance in the context of a computer program, we are referring to how quickly or efficiently that program can execute.
Polymorphic functions. A polymorphic function is a function that changes its behavior based on the types of arguments that are passed to it.
The key word here is types, as opposed to values. (A function that didn’t change its output based on different values for arguments would not be a very useful function at all.)
JavaScript engine. In order to think about performance productively, we also need to know where our JavaScript is going to be executed. For our example code, we’ll use the V8 engine given its popularity.
V8 is the engine that powers the Chrome browser, Node.js, the Edge browser, and more. Note that there are also other JavaScript engines with their own performance characteristics, such as SpiderMonkey (used by Firefox), JavaScriptCore (used by Safari), and others.
Creating a polymorphic function in JavaScript
Suppose we are building a JavaScript library that enables other engineers to easily store messages to an in-memory database with our simple API. In order to make our library as easy and comfortable to use as possible, we provide a single polymorphic function that is very flexible with the arguments that it receives.
Option 1: Use completely separate arguments
The first signature of our function will take the required data as three separate arguments, and can be called like this:
Option 2: Use message contents with options
object
This signature will allow consumers to separate the required data (message contents) from the optional data (the author and the timestamp) into two separate arguments. We’ll accept the arguments in any order, for convenience.
Option 3: Use an options
object
We’ll also allow users of our API to call the function passing in a single argument of an object containing all of the data we need:
Option 4: Use only the message contents
Finally, we’ll allow users of our API to provide only the message contents, and we’ll provide default values for the rest of the data:
Implementing a polymorphic function
OK, with our API defined, we can build the implementation of our polymorphic function.
OK, now we’ll write some code that stores a lot of messages using our function — taking advantage of its polymorphic API — and measure its performance.
Now let’s implement our function again but with a simpler, monomorphic API.
Creating a monomorphic function in JavaScript
In exchange for a more restrictive API, we can trim down the complexity of our function and make it monomorphic, meaning that the arguments of the function are always of the same type and in the same order.
Although it won’t be as flexible, we can keep some of the ergonomics of the previous implementation by utilizing default arguments. Our new function will look like this:
We’ll update the performance measuring code from our previous example to use our new unified API.
Results
OK, now let’s run our programs and compare the results.
The monomorphic version of our function is about twice as fast as the polymorphic version, as there is less code to execute in the monomorphic version. But because the types and shapes of the arguments in the polymorphic version vary widely, V8 has a more difficult time making optimizations to our code.
In simple terms, when V8 can identify (a) that we call a function frequently, and (b) that the function gets called with the same types of arguments, V8 can create “shortcuts” for things like object property lookups, arithmetic, string operations, and more.
For a deeper look at how these “shortcuts” work I would recommend this article: What’s up with monomorphism? by Vyacheslav Egorov.
Pros and cons of polymorphic vs monomorphic functions
Before you go off optimizing all of your code to be monomorphic, there are a few important points to consider first:
Polymorphic function calls are not likely to be your performance bottleneck. There are many other types of operations that contribute much more commonly to performance problems, like latent network calls, moving large amounts of data around in memory, disk i/o, complex database queries, to name just a few.
You will only run into performance issues with polymorphic functions if those functions are very, very “hot” (frequently run). Only highly specialized applications, similar to our contrived examples above, will benefit from optimization at this level. If you have a polymorphic function that runs only a few times, there will be no benefit from rewriting it to be monomorphic.
You will have more luck updating your code to be efficient rather than trying to optimize for the JavaScript engine. In most cases, applying good software design principles and paying attention to the complexity of your code will take you further than focusing on the underlying runtime. Also, V8 and other engines are constantly getting faster, so some performance optimizations that work today may become irrelevant in a future version of the engine.
Conclusion
Polymorphic APIs can be convenient to use due to their flexibility. In certain situations, they can be more expensive to execute, as JavaScript engines cannot optimize them as aggressively as simpler, monomorphic functions.
In many cases, however, the difference will be insignificant. API patterns should be based on other factors like legibility, consistency, and maintainability because performance issues are more likely to crop up in other areas anyway. Happy coding!