March 29, 2013

Response to the .NET vs Node.JS performance post

Here's a response to this blog post

Since the response time looks to be a linear function of the number of parallel requests, I'm just going to run the tests for 50/100/150 requests/sec.

First I'm going to get the .NET performance numbers on my machine (Windows 7):

 50 r//        1169 ms
100 r//        2214 ms
150 r//        3703 ms

And the original Node.js test on node 10.2:

 50 r//        2973 ms
100 r//        6245 ms
150 r//        9548 ms

Now let's make the comparison a bit more fair, the first thing I'm going to do is to remove the async.js depedency, because it's pretty much useless.

var http = require('http');
var fs = require('fs');

http.createServer(function(request, response) {
    var file = parseInt(request.url.substring(1));
    file = file % 190;
    file = String("000" + file).slice(-3);

    // read the file
    fs.readFile('../data/input'+file+'.txt', 'ascii', function(err, data) {
        if(err) {
            response.writeHead(400, {'Content-Type':'text/plain'});
            response.end();
        }
        else {
            var results = data.toString().split("\r\n")
                .map(function(item){ return parseFloat(item); });

            results.sort();

            response.writeHead(200, {'Content-Type': 'text/plain'});
            response.end('input'+file+'.txt\t' + results[(parseInt(results.length/2))]);
        }
    });
}).listen(8080, '127.0.0.1');

console.log('Server running at http://127.0.0.1:8080/')

Let's take a look at the numbers:

 50 r//        3071 ms
100 r//        6208 ms
150 r//        9316 ms

Ok the async.js is clearly not a big issue, but it couln't hurt to remove it.

Now, if we look at the C# code, we see that it's actually sorting on strings, so why should we convert to floats in node?

var http = require('http');
var fs = require('fs');

http.createServer(function(request, response) {
    var file = parseInt(request.url.substring(1));
    file = file % 190;
    file = String("000" + file).slice(-3);

    // read the file
    fs.readFile('../data/input'+file+'.txt', 'ascii', function(err, data) {
        if(err) {
            response.writeHead(400, {'Content-Type':'text/plain'});
            response.end();
        }
        else {
            var results = data.toString().split("\r\n");

            results.sort();

            response.writeHead(200, {'Content-Type': 'text/plain'});
            response.end('input'+file+'.txt\t' + results[(parseInt(results.length/2))]);
        }
    });
}).listen(8080, '127.0.0.1');

console.log('Server running at http://127.0.0.1:8080/')

Now we are probably have a fair comparison:

 50 r//        1661 ms
100 r//        3245 ms
150 r//        5132 ms

We can see that the gap is clearly not as big as initially stated.

To go a bit further, we can try to add threads to the mix and take a look at the results:

var http = require('http');
var fs = require('fs');

var cluster = require('cluster');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    // Fork workers.
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    // Workers can share any TCP connection
    http.createServer(function(request, response) {
        var file = parseInt(request.url.substring(1));
        file = file % 190;
        file = String("000" + file).slice(-3);

        // read the file
        fs.readFile('../data/input'+file+'.txt', 'ascii', function(err, data) {
            if(err) {
                response.writeHead(400, {'Content-Type':'text/plain'});
                response.end();
            }
            else {
                var results = data.toString().split("\r\n");

                results.sort();

                response.writeHead(200, {'Content-Type': 'text/plain'});
                response.end('input'+file+'.txt\t' + results[(parseInt(results.length/2))]);
            }
        });
    }).listen(8080, '127.0.0.1');
}

console.log('Server running at http://127.0.0.1:8080/')

I have a i7 Q740 so I should see improvements:

 50 r//        820 ms
100 r//        1319 ms
150 r//        3166 ms

Nice isn't it, better than the single threaded .NET but it's not fair for .NET this time. To be complete, I should try to run the .NET server multithreaded, since the HttpListener can't share connections like the node.js one, it's a bit more difficult. I'll try to update this if I can get the multithreaded .NET version to work.

UPDATE

Turns out the .NET Task system uses the thread pool by default (checked with Thread.CurrentThread.ManagedThreadId) so the .NET version is already multithreaded, and that makes Node.JS the winner.