Programming Topics + Overload Journal #155 - February 2020
Browse in : All > Topics > Programming
All > Journals > Overload > o155
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: A line-to-line conversion from Node.js to Node.cpp

Author: Bob Schmidt

Date: 05 February 2020 18:57:27 +00:00 or Wed, 05 February 2020 18:57:27 +00:00

Summary: Dmytro Ivanchykhin, Sergey Ignatchenko and Maxim Blashchuk show how we can get a 5x improvement in speed.

Body: 

Disclaimer: as usual, the opinions within this article are those of ‘No Bugs’ Hare, and do not necessarily coincide with the opinions of the translators and Overload editors; also, please keep in mind that translation difficulties from Lapine (like those described in [Loganberry04]) might have prevented an exact translation. In addition, the translators and Overload expressly disclaim all responsibility from any action or inaction resulting from reading this article.

It is pretty well known that, computation-wise, C++ code is substantially faster than Node.js (even with all the efforts spent on the v8 engine); for details we can refer to [Debian], where for different calculation-oriented algorithms the results were in favour of C++, with differences in wall-clock time ranging from 1.2x to 6x.

However, most of the benchmarks out there (including [Debian]) are concentrating on pure computations. This means that differences in the ability of different frameworks to handle requests (which forms a basis for handling real-world interactive loads) are not addressed. This article aims to start covering this gap.

Node.cpp

At this point, we want to compare good old Node.js with our own new kid on the block, which we named Node.cpp.

The idea behind Node.cpp is to make a framework which will allow us to write C++ code in Node.js style while benefiting from C++ goodies (including significantly improved performance). Moreover,

It should be possible to take existing Node.js code and convert it into Node.cpp with line-to-line correspondence between the two.1

As of now, Node.cpp is still very much in its infancy, but we have already managed to write enough code to run some benchmark tests Let’s take a look at the code of the http ‘echo’ server which is along the lines of the sample from [Ostinelli11] (see Listing 1).

//Node.js
http.createServer(function(request, response) {
  if ( request.method == "GET" 
       || request.method == "HEAD" ) {
    response.writeHead(200, 
      {"Content-Type":"text/xml"});
    var urlObj = url.parse(request.url, true);
    var value = urlObj.query["value"];
    if (value == ''){
      response.end("no value specified");
    } else {
      response.end("" + value + "");
    }
  } else {
    response.writeHead(405, 
      "Method Not Allowed");
    response.end();
  }
}).listen(2000);

//Node.cpp
srv = net::createHttpServer<ServerType>(
   [](net::IncomingHttpMessageAtServer& request,
      net::HttpServerResponse& response){
      if ( request.getMethod() == "GET" 
           || request.getMethod() == "HEAD" ) {
        response.writeHead(200, 
          {{"Content-Type", "text/xml"}});
        auto queryValues =
          Url::parseUrlQueryString(
          request.getUrl() );
        auto& value = queryValues["value"];
        if (value.toStr() == ""){
       response.end("no value specified");
     } else {
       response.end( value.toStr() );
     }
      } else {
        response.writeHead( 405, 
          "Method Not Allowed");
        response.end();
      }
  });
srv->listen(2000, "0.0.0.0", 5000);
            
Listing 1

As we can see, there is a direct correspondence between Node.JS code and Node.cpp code; sure, there are quirks related to the nature of C++ (and some more due to still-missing APIs in Node.cpp – which will be fixed before release), but overall the whole thing looks similar enough

to enable manual but more or less mechanistic rewriting from Node.js into Node.cpp

Sure, after rewriting into Node.cpp the code will be still a bit more verbose, but what is important is that such a rewrite should be feasible without any changes to the essence of the Node.js code.

What’s the point?

Ok, we can see that it IS possible to convert some rudimentary Node.js code into Node.cpp without breaking the structure of existing Node.js code. But what is the point of going through such an exercise?

In the not so distant future, we’re planning to add some ultra-useful features to Node.cpp – such as deterministic recording/replay (which in turn will allow production post-mortem debugging (sic!)); however, for the time being, we’ll concentrate on one advantage of Node.cpp – namely, on performance.

Test setup

When testing our Node.cpp against Node.js, our plan was to:

To achieve this, we used httperf along the lines described in [Ostinelli11], with some changes intended (a) to reflect the real world better (most importantly, we ran our tests between two separate boxes), and (b) restricting the programs under test to use one single core (multi-core tests using cluster module are coming, but they’re not a part of this particular article).

Hardware

Unlike [Ostinelli11], we ran our client and server on two different boxes:

Client and server boxes were interconnected directly via a 10Gbit switch.

Software

Both client and server boxes were running stock Ubuntu 19.10 (Eoan).

On the client, we ran httperf patched to use an increased number of file descriptors as discussed in [Stackoverflow].

We used this command line for httperf:

  httperf --timeout=5 --client=0/1
  --server=10.32.36.3 --port=2000  --rate=XXX
  --send-buffer=4096 --recv-buffer=16384
  --num-conns=8000 --num-calls=70

where the (session) rate parameter went from 100 to 2000 in steps of 100.

On the server, we ran either Node.js, which is available for eoan (10.15.2), or the open-source code for [node.cpp] compiled with Clang 9. App-level code used for Node.js and Node.cpp is shown in Listing 1 above. Another test we ran (just as a sanity check and to put things into perspective) was the raw performance of the single-core nginx serving static files (which are requested by the same httperf); in a sense, nginx results represent ‘The Holy Grail’ of http dynamic processing, something we can try to reach.

Results

The results of our testing are shown in Figure 1.

Figure 1

Here, along the lines of [Ostinelli11], the ‘desired’ response rate is calculated as rate × num-calls (as specified in the command line for httperf), and the ‘real’ response rate is the actual response rate as measured by httperf.

As we can see, with a lower load, all the servers behave similarly until the capacity of a particular server is reached; but after that limit is reached, however much we increase the load, the response rate doesn’t really improve and stays more or less stable.

As such, we can conclude (at least within the limitations of the current test setup), that Node.js can handle a maximum of 13,000 responses/sec per core, while Node.cpp can handle around 70,000 responses/sec (that’s over a 5× advantage performance-wise(!!)). Static-serving nginx, as expected, goes well above both dynamic handlers (at 100,000 responses/sec), but we have to note that (i) a 30% difference between Node.cpp and nginx is not THAT bad, and (ii) we will try to bring node.cpp MUCH closer to the performance of static-serving nginx.

Important notes about the results:

Great! When can we start conversion?

As noted above, the whole point of Node.cpp is to allow conversion of (those 5% of performance-critical Nodes which warrant such an effort) from Node.js into Node.cpp.

The only tiny problem on the way is that Node.cpp is still in its early infancy, and is not ready (yet) for production use. In particular, the set of supported APIs is still extremely limited, and package management is not there yet. If you want to help, please feel free to contribute (or contact the authors); we feel that Node.cpp is a project with wonderful prospects that can help make Node.* ecosystem an indisputable leader at least performance-wise (and a common point of view is that Node loses performance-wise both to Erlang/Elixir and to Golang [Christensen16] [Peabody] [Stressgrid20]2).

Conclusions and future work

Over the course of this article, we took a very rudimentary Node.js http server, and converted it more or less line-by-line into Node.cpp. Then we ran a bunch of http tests to compare performance of both versions, and found that the Node.cpp server outperforms the Node.js one by a factor of 5×. We feel that this opens up wonderful opportunities and are going to continue our work to enable high-performance Node.* programming for those Nodes where performance is critical.

Acknowledgement

Cartoon by Sergey Gordeev from Gordeev Animation Graphics, Prague.

References

[Christensen16] Jack Christensen (2016) ‘Websocket Shootout: Clojure, C++, Elixir, Go, NodeJS, and Ruby’ at: https://hashrocket.com/blog/posts/websocket-shootout, posted 1 September 2016

[Debian] The Computer Language Benchmarks Game: ‘Node js versus C++ g++ fastest programs’ https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/node-gpp.html

[Loganberry04] David ‘Loganberry’, Frithaes! – an Introduction to Colloquial Lapine!, http://bitsnbobstones.watershipdown.org/lapine/overview.html

[Ostinelli11] Roberto Ostinelli (2011) ‘A comparison between Misultin, Mochiweb, Cowboy, NodeJS and Tornadoweb’ at: http://www.ostinelli.net/a-comparison-between-misultin-mochiweb-cowboy-nodejs-and-tornadoweb/

[node.cpp] node.cpp, https://github.com/node-dot-cpp/node.cpp

[Peabody] Brad Peabody ‘Server-side I/O Performance: Node vs. PHP vs. Java vs. Go’ at: https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go

[Stackoverflow] lawnmowerlatte ‘Changing the file descriptor size in httperf’ at: https://stackoverflow.com/a/16449853/4947867

[Stressgrid20] Stressgrid (2020) ‘Benchmarking Go vs Node vs Elixir’ at: https://stressgrid.com/blog/benchmarking_go_vs_node_vs_elixir/, posted 6 January 2020

  1. At this point, we’re talking about a manual rewrite; whether automated conversion will be possible, and how efficient it will be, is currently beyond the scope of this article.
  2. We do know that at least some of these tests are unfair to Node.js (by ignoring cluster module), and that Node is generally rather competitive to Erlang/Elixir and Golang, but 5x performance improvement would clearly blow all the competition out of the water.

Dmytro Ivanchykhin has 10+ years of development experience, and has a strong mathematical background (in the past, he taught maths at NDSU in the United States).

Sergey Ignatchenko has 15+ years of industry experience, including being a co-architect of a stock exchange, and the sole architect of a game with 400K simultaneous players. He currently holds the position of Security Researcher.

Maxim Blashchuk has substantial development experience, most of it with embedded programming. Recently he joined a team performing research on low-level C++ libraries providing properties such as determinism and memory safety.

Notes: 

More fields may be available via dynamicdata ..