I’m always excited to try out new languages. I’m not an expert in the field, but my two cents are that:
- You cannot optimize for both expressiveness and speed of execution at the same time; if you optimize for one of them, the other has to suffer.
- Even so, there is a sweet spot to reach somewhere in the middle, and it hasn’t been reached so far by today’s languages. Take, for example:
Python, Ruby and others — expressive but terribly, terribly slow.
Java, Go and others — fairly speedy, but very inexpressive or complex. Lots of boilerplate needed in order to get stuff done generally.
C, C++ — the fastest things out there, but incredibly bad for development; C makes it very easy to shoot yourself in the foot, and by the time you’re done writing boilerplate code, you forgot what you were trying to do. C++ is a way too complex language for any developer to fully comprehend, and both are painfully slow to compile.
Where does Nim fit in here? First, the good parts:
First, speed. Nim compiles into C code at the moment, and it’s supposedly very fast. Here’s a quick (not realistic, not comprehensive) benchmark that I’ve done for a quick test. The code counts the prime numbers below 100 million, by using Erathostenes’ Sieve. Here is my C version:
#include <stdio.h>
#define SIEVE_UP_TO 100000000
int sieve[SIEVE_UP_TO];
int main() {
int i,j;
for(i=2; i<SIEVE_UP_TO; ++i) {
if(!sieve[i]) {
for(j=2*i; j<SIEVE_UP_TO; j+=i) {
sieve[j] = 1;
}
}
}
int num = 0;
for(i=2; i<SIEVE_UP_TO; i++) {
if(!sieve[i]) {
++num;
}
}
printf("%d", num);
return 0;
}
Here is the Nim version:
var sieve:array[0..100_000_000, bool]
for i in 2..(sieve.high div 2):
if not sieve[i]:
for j in countup(2*i, sieve.high, i):
sieve[j] = true
var num = 0
for i in 2..(sieve.high):
if not sieve[i]:
num.inc
echo num
And here’s the Python version:
sieve_size = 100000000
sieve = [0] * sieve_size
for i in range(2, sieve_size / 2):
if sieve[i] == 0:
for j in range(2*i, sieve_size, i):
sieve[j] = 1
print(sieve_size - sum(sieve) - 2)
A few things to note:
- They all use the same non-optimized algorithm
- The C version is by far the most verbose of all
- The Nim and Python versions are similar in size, and they even look quite similar, with significant whitespace (whether you’re into that or not), ranges and for .. in.
I’ve ran them all, with the following settings:
$ gcc -O2 eratosthenes.c && time ./a.out
$ nim c --opt:speed -d:release eratosthenes.nim && time ./eratosthenes
$ time python eratosthenes.py
$ time pypy eratosthenes.py
And here are the results:
- C version: ~1.7s
- Nim version: ~1.8s
- Python version: ~60s
- Pypy version: ~3.8s
I find this quite impressing given that Nim is very easy to write and presumably much safer than C. At a glance it seems to strike a good balance between ease of development and speed of execution.
Another notable feature that Nim brings to the table is powerful macros. For example, you can create a Sinatra-style DSL that compiles down to very efficient machine code for a simple web server:
import jester, asyncdispatch, htmlgen
routes:
get "/":
resp h1("Hello world")
runForever()
Other notable features include futures / await, easy C interop, generics, functional programming and other cool things.
It seems to be a young language that’s still rough around the edges, but it shows great promise of achieving good ease of development and performance.