I offer the readers of Habrakhabr a translation of the article “A Quick Comparison of Nim vs. Rust . My comments will be in italics.Rust and
Nim are two new programming languages for which I follow the development. Soon, after my
first post about Rust,
Nim 0.10.2 was published . This prompted me to get to know Nim and, of course, compare it with Rust.
In this post I am going to show you two simple programs written in Nim and Rust with a rough comparison of their execution times and will express my subjective impressions of programming in these languages.
')
Example # 1: Word counting (wordcount)
This example uses file I / O, regular expressions, hash tables (associative arrays), and parsing the arguments passed to the command. As the name implies, the program counts the number of words in files or
stdin .
Usage example:
Usage: wordcount [OPTIONS] [FILES] Options: -o:NAME set output file name -i
If we pass the argument
-i , the result will be as follows:
2 case 1 file 1 files 1 h 2 help ...
Nim version
The program on Nim is quite simple. It uses
tables .
CountTable to count words,
parseopt2 .
getopt for parsing command and
sequtils arguments .
mapIt for functional mapping operation. For regular expressions, I chose the
pegs module, which is recommended by the Nim documentation instead of
re .
The
{.raises: [IOError].} Directive on line 3 guarantees that the
doWork procedure throws only an
IOError exception. To do this, I put
input.findAll (peg "\ w +") inside a
try expression in line 21 to catch exceptions that, theoretically, can occur.
Part of the
wordcount.nim code:
proc doWork(inFilenames: seq[string] = nil, outFilename: string = nil, ignoreCase: bool = false) {.raises: [IOError].} = # Open files var infiles: seq[File] = @[stdin] outfile: File = stdout if inFilenames != nil and inFilenames.len > 0: infiles = inFilenames.mapIt(File, (proc (filename: string): File = if not open(result, filename): raise newException(IOError, "Failed to open file: " & filename) )(it)) if outFilename != nil and outFilename.len > 0 and not open(outfile, outFilename, fmWrite): raise newException(IOError, "Failed to open file: " & outFilename) # Parse words var counts = initCountTable[string]() for infile in infiles: for line in infile.lines: let input = if ignoreCase: line.tolower() else: line let words = try: input.findAll(peg"\w+") except: @[] for word in words: counts.inc(word) # Write counts var words = toSeq(counts.keys) sort(words, cmp) for word in words: outfile.writeln(counts[word], '\t', word)
Rust version
For a better understanding of Rust, I implemented a simple
BTreeMap structure akin to
collections :: BTreeMap , but ultimately I used
collections :: HashMap for a fair comparison with Nim (the
BTreeMap code remained in the repository for review). The
getopts package is used to parse the command arguments into my
Config structure. Then everything should be clear.
Part of the code from my
Rust wordcount project:
fn do_work(cfg: &config::Config) -> io::Result<()> {
Zachary Dremann suggested a
pull request using
find_iter . I left
captures_iter for consistency with the Nim version, but improved my code a bit.
Runtime comparison
I compiled the code with the
-d: release flags for Nim and
--release for Rust. For example, I took a 5 megabyte file compiled from the Nim compiler sources:
$ cat c_code/3_3/*.c > /tmp/input.txt $ wc /tmp/input.txt 217898 593776 5503592 /tmp/input.txt
The command to run the program:
$ time ./wordcount -i -o:result.txt input.txt
Here is the result on my Mac mini with a 2.3 GHz Intel Core i7 processor and 8 GB of memory: (1x = 0.88 seconds)
Rusty | regex! \ w | Regex \ w | regex! [...] | Regex [...] | Nim |
release, -i | 1x | 1.30x | 0.44x | 1.14x | 0.75x |
release | 1.07x | 1.33x | 0.50x | 1.24x | 0.73x |
debug, -i | 12.65x | 20.14x | 8.77x | 19.42x | 3.51x |
debug | 12.41x | 20.09x | 8.84x | 19.33x | 3.25x |
Notes:
- In the Rust regex! works faster than Regex , and r "[a-zA-Z0-9 _] + is faster than r" \ w + " . All 4 combinations were tested.
- The debug version is just for comparison.
- Nim works 1-2% slower with the --boundChecks: on flag, I didn’t add this result to the example.
Example number 2: the game "Life"
This example launches
Life in the console with a fixed field size and a template (edit the source code to change the size or pattern). It uses
ANSI CSI code to redraw the screen.
After launch, the screen will look something like this:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . (). . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . (). . . . . . . . . . . . ()(). . . . . . . . . . . . . . ()()()(). . . . (). . . . (). . . . . . . . ()(). . ()(). . . . . . . . . ()(). (). (). . . . (). . . . . . . . . . . . . . . . ()(). . . . . . . . ()()(). (). . (). . . (). . . (). . . . . . . . . . . . . . . . . . . . . . . ()(). (). (). . . . . . (). (). . . . . . . . . . . . . . . . . . . . . . . . ()()()(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ()(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ()()(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . n = 300 Press ENTER to exit
The program uses another stream to read from
stdin and interrupts the game when it receives any character.
Nim version
Here is part of the code from my
Nim conway project:
type Cell = bool ConwayMap* = array[0.. <mapHeight, array[0.. <mapWidth, Cell]] proc init*(map: var ConwayMap, pattern: openarray[string]) = ## Initialise the map. let ix = min(mapWidth, max(@pattern.mapIt(int, it.len))) iy = min(mapHeight, pattern.len) dx = int((mapWidth - ix) / 2) dy = int((mapHeight - iy) / 2) for y in 0.. <iy: for x in 0.. <ix: if x < pattern[y].len and pattern[y][x] notin Whitespace: map[y + dy][x + dx] = true proc print*(map: ConwayMap) = ## Display the map. ansi.csi(AnsiOp.Clear) ansi.csi(AnsiOp.CursorPos, 1, 1) for row in map: for cell in row: let s = if cell: "()" else: ". " stdout.write(s) stdout.write("\n") proc next*(map: var ConwayMap) = ## Iterate to next state. let oldmap = map for i in 0.. <mapHeight: for j in 0.. <mapWidth: var nlive = 0 for i2 in max(i-1, 0)..min(i+1, mapHeight-1): for j2 in max(j-1, 0)..min(j+1, mapWidth-1): if oldmap[i2][j2] and (i2 != i or j2 != j): inc nlive if map[i][j]: map[i][j] = nlive >= 2 and nlive <= 3 else: map[i][j] = nlive == 3
Rust version
Here is part of the code from my
Rust conway project:
type Cell = bool; #[derive(Copy)] pub struct Conway { map: [[Cell; MAP_WIDTH]; MAP_HEIGHT], } impl Conway { pub fn new() -> Conway { Conway { map: [[false; MAP_WIDTH]; MAP_HEIGHT], } } pub fn init(&mut self, pattern: &[&str]) { let h = pattern.len(); let h0 = (MAP_HEIGHT - h) / 2; for i in 0..(h) { let row = pattern[i]; let w = row.len(); let w0 = (MAP_WIDTH - w) / 2; for (j, c) in row.chars().enumerate() { self.map[i + h0][j + w0] = c == '1'; } } }
In line 49, I defined a variable to track the change in the display, but a simple comparison of
self.map! = Newmap does not work for arrays longer than 32 elements, unless you implement the PartialEq
treit .
Note that I used insecure
libc :: exit in my
main.rs , which is not very typical for Rust. Zachary Dremann suggested a
pull request in which
libc :: exit is elegantly avoided using the
select macro
! and non-blocking timer. You might want to watch.
Runtime comparison
To compare the execution time, you need to make some changes in the code:
- Comment out the sleep call in conway.nim and
main.rs
- Change the number of loop iterations from 300 to 3000
- Redrawing a field spends a lot of time, so two measurements were made (1) with redrawing and (2) without it (i.e., with commented out lines for printing a field in conway.nim and main.rs )
Here are the results when compiling with the
-d flags
: release for Nim and
--release for Rust:
| Rusty | Nim | Nim / bc: on | n = 30,000 |
(1) with map print | 1x | 1.75x | 1.87x | 1x = 3.33s |
(2) without map print | 1x | 1.15x | 1.72x | 1x = 0.78 |
Since Rust does a check for going beyond the list, for justice, I added a
Nim / bc: on column for the Nim version compiled with the
--boundChecks: on flag.
Nim or Rust
Although Nim and Rust are compiled languages with good performance, they are very different. For me, their similarities are as follows:
- compiled and statically typed
- calculation for good performance (each of them can work faster depending on the implementation of the program and its further optimizations)
- composition instead of inheritance (similar to the trend in new languages?)
- simple bundle with C
- popular language delicacies: generics, closures, functional approaches, type inference, macros, operators in the form of instructions, etc.
But their differences are more interesting.
Philosophy: freedom or discipline
When programming on Nim, it seems that you write in a scripting language. He really blurs the line. Nim tries to get rid of the noise in the code as much as possible, and therefore it is fun to program on it.
However, there is a downside to such freedom: clarity, purity and sustainability may suffer. Here is a small example: in Nim
import imports all module names into your namespace. Names from an imported module can be limited to using the
module.symbol syntax or using
from module import nil for controlled import of names, but tell me who uses it? Moreover, this approach is not typical for Nim. As a result, you will not be able to understand which names came from which module when reading someone else's (or your own) code (fortunately, there are no contradictions of names, because in such cases Nim makes it impossible to separate flies from cutlets).
Other examples:
UFCS allows you to use
len (x) ,
len x ,
x.len () or
x.len as you like; does not share names with underscore and different case, so
mapWidth ,
mapwidth and
map_width will be converted to the same name (I am glad that they included the "partial case sensitivity" rule in version 0.10.2, therefore
Foo and
foo will be considered different names) ; in the order of things is the use of uninitialized variables. In theory, you can follow strict coding principles, but when programming in Nim you will feel more relaxed.
On the other hand, Rust honors discipline. His compiler is very strict. Everything should be very clear. You get the right approaches in advance. Ambiguity is not about the code on Rust ...
This approach is usually good for long-lived projects and for maintainability, but when programming with Rust, you start to take care of such details that you might not be interested in at all. You start thinking about using memory or increasing performance, even if it is not a priority for your task. Rust makes you more disciplined.
Both have their pros and cons. As a programmer, I enjoy Nim more; as a maintainer, I'd rather accompany products written in Rust.
Visual style: Python or C ++
Like Python, Nim uses padding to separate blocks of code and there are fewer
characters than it. Rust is more like C ++.
{} ,
:: ,
<> and
& will be familiar to C ++ programmers, plus Rust adds some new things like
'a .
Sometimes Nim can be too literal. For example, I think the
match syntax in Rust:
match key.cmp(&node.key) { Less => return insert(&mut node.left, key, value), Greater => return insert(&mut node.right, key, value), Equal => node.value = value, }
looks cleaner than the Nim
case expression
case key of "help", "h": echo usageString of "ignore-case", "i": ignoreCase = true of "o": outFilename = val else: discard
But, in general, the code at Nim is less noisy. In my opinion, the special parameters in Rust are introduced by the
lifetime parameters and this will not change.
Memory Management: Garbage Collector or Manual Control
Although Nim allows unsafe memory management and provides support for managing the garbage collector in runtime for more predictable behavior. It is still a language with a garbage collector, which has all the advantages and disadvantages of it. Objects in Nim are assigned copies of the values. If the garbage collector doesn’t hurt your task, memory management in Nim will not cause you problems.
Rust provides limited support for the garbage collector, but more often you will rely on a memory management system. As a Rust programmer, you must fully understand his memory management model (ownership, borrowing, and lifetime) before you start writing programs efficiently, which is the first barrier for newbies.
On the other hand, this is also the strength of Rust - secure memory management without the use of a garbage collector. Rust copes with this task. Along with the security of shared resources, the security of concurrent data access and the elimination of null Rust pointers is an extremely reliable programming language with no overhead for resource consumption at runtime.
Depending on your requirements, either the Nim garbage collector will be enough for you, or your choice will fall on Rust.
Other differences
Nim's strengths:
- Productivity: in the same time frame, you will overwrite more features in Nim
- Ease of learning
- Compiled language as a scripting language, good for prototyping, interactive research, batch processing, etc.
- Fishechki:
- method redefinition
- definition of new operators
- named arguments and default values
- powerful macros
Rust's strengths:
- This system programming language: embedded, without garbage collector, close to hardware
- Safe, disciplined, reliable
- Strong core team and active community
- Fishechki:
- excellent pattern matching implementation
- enumerations (enum) , although enums are also good in Nim
- let mut instead of var (small but important thing)
- powerful syntax for dereferencing structures
Error handling: Nim uses a generic exception mechanism, Rust uses the return type
Result (and the macro
panic! ). I have no preference for this, but I found it important to mention this distinction.
Release 1.0 is coming soon
Nim and Rust should be released this year
(Rust is released ) . It's great! Rust has already received quite a lot of attention, but Nim is becoming more famous. They are very different in taste, but both are great new programming languages. Rust shows its best in terms of performance and security. Nim agile
(pun: Nim is nimble) , expressive, realizes the strengths of scripting and compiled languages. Both of them will be a great addition to your toolkit.
I hope, after reading this article, you have made your opinion about these programming languages.