Here I am again with an article about Node.js! Today I want to speak about another Node.js advantage — execution speed. What do I mean by “execution speed”?
It can be anything ranging from calculating Fibonacci sequence to querying database.
When talking about web-services, execution speed comprises everything that is needed to process requests and send the response to the client. That’s what I mean — time spent on processing request, starting from opening connection to client receiving the response.
As soon as you understand what’s going on in Node.js server when it processes the requests, you will realise why it is so fast.
But before talking about Node.js, let’s look at how request handling is done in other languages. PHP is the best example because it is popular and unoptimised.
Here is a list of drawbacks that decrease execution performance in PHP:
- PHP is synchronous. It means when you are processing a request, writing to the database, for instance, it blocks any other operations, so you will have to wait for it to finish, before you can do anything else.
- Each request to web-service creates a separate PHP interpreter process that is running your code. Thousands of connections means thousands of running processes that take your RAM. You can see how your used memory is growing up with your active connections.
- PHP doesn’t have a JIT compilation. It’s important when you have code that is executed often and you want to be sure that this code is as close to machine code as possible for better performance.
These are only the most critical problems with PHP, but there are a lot more, in my humble opinion.
Now let’s see how Node.js handles these problems.
- Node.js is single-threaded and asynchronous. Every I/O operation doesn’t block other operations. This means you can read files, send emails, query the database, etc. and at the same time.
Now we’ve seen advantages of asynchronous concept, let me explain how it works in Node.js.
Know your asynchronous!
Let me give you an example of an asynchronous processing concept (thanks to Kirill Yakovenko).
Imagine you had 1,000 balls on the top of the mountain. You need to push them all to the bottom of the mountain. You can’t really push them all at the same time, you’ll have to push them one by one, but that doesn’t mean that you have to wait for each ball to get to the bottom of the mountain before pushing the next one.
In this example, synchronous execution would wait for a ball to roll down to the bottom before pushing another one, which is a waste of time.
Asynchronous execution would push all the balls one by one, then waiting for them all to get down (receiving a notification) and collecting results.
How does this help in web-server performance?
Let’s say that each ball is one query to the database. You have a big project with many queries, aggregations, etc. When you are processing all the data synchronously, it blocks the code execution. When you are processing it asynchronously, you can execute all the queries at once and then just collect the results.
In the real world, when you have a lot of connections, it improves performance.
How is the asynchronous concept implemented in Node.js?
Event loop is a construction responsible for dispatching events in a program that almost always operates asynchronously with the message originator. When you call an I/O operation, Node.js stores the callback assigned with that operation and continue processing other events. Callback will be triggered when all needed data are collected.
Here is a more advanced definition of the event loop:
The event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. It works by making a request to some internal or external “event provider” (which generally blocks the request until an event has arrived), and then it calls the relevant event handler(“dispatches the event”). The event-loop may be used in conjunction with a reactor, if the event provider follows the file interface, which can be selected or ‘polled’ (the Unix system call, not actual polling). The event loop almost always operates asynchronously with the message originator.
Let’s look at a simple graph that explains how event loop works in Node.js.
When a request is received by web-server, it goes to the event loop. Event loop registers operation in a thread pool with assigned callback. Callback will be triggered when processing requests are done. Your callback also can do other intensive operations like querying the database, but it does so the same way — registers operation in a thread pool with assigned callback and so on…
How V8 optimises your code?
Thanks to Wingolog, where it is described how V8 works, I can simplify that material and make a thesis.
V8 has two types of compilers (There is also a third compiler in development which is called Turbofan): “Full” compiler and “Crankshaft” compiler.
When function was compiled and code is running, V8 starts the profiler thread to see, which functions are hot and which are not, also collecting the type feedback, so V8 can record the type information flowing through it.
Once V8 has identified that a function is “hot” and has some type feedback information, it tries to run the augmented AST through an optimising compiler — “Crankshaft” compiler.
The “Crankshaft” compiler doesn’t run as fast but tries to produce the optimised code. It comprises two components: Hydrogen and Lithium.
Hydrogen compiler builds CFG (Control Flow Graph) from AST (based on type feedback information). This graph is presented in SSA (Static Single Assignment) form. Based on simple structure of HIR (High-Level Intermediate Representation) and SSA form, compiler can apply many optimisations like constant folding, method inlining, etc…
Lithium compiler translates the optimised HIR into a LIR (Low-Level Intermediate Representation). The LIR is similar to machine code, but still platform-independent. In contrast to HIR, LIR form is closer to a three-address code.
Only then this optimised code replace the old unoptimised code and continue executing your code much faster.
Eugene Obrezkov, Technical Leader and Consultant at Onix-Systems, Kirovohrad, Ukraine.