📜 ⬆️ ⬇️

Compare Nim and Rust

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 --ignore-case ignore case -h --help print this help menu 


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<()> { // Open input and output files let mut readers = Vec::with_capacity(std::cmp::max(1, cfg.input.len())); if cfg.input.is_empty() { readers.push(BufReader::new(Box::new(io::stdin()) as Box<Read>)); } else { for name in &cfg.input { let file = try!(File::open(name)); readers.push(BufReader::new(Box::new(file) as Box<Read>)); } } let mut writer = match cfg.output { Some(ref name) => { let file = try!(File::create(name)); Box::new(BufWriter::new(file)) as Box<Write> } None => { Box::new(io::stdout()) as Box<Write> } }; // Parse words let mut map = collections::HashMap::<String, u32>::new(); let re = regex!(r"\w+"); // let re = Regex::new(r"\w+").unwrap(); // let re = regex!(r"[a-zA-Z0-9_]+"); // let re = Regex::new(r"[a-zA-Z0-9_]+").unwrap(); for reader in &mut readers { for line in reader.lines() { for caps in re.captures_iter(&line.unwrap()) { if let Some(cap) = caps.at(0) { let word = match cfg.ignore_case { true => cap.to_ascii_lowercase(), false => cap.to_string(), }; match map.entry(word) { Occupied(mut view) => { *view.get_mut() += 1; } Vacant(view) => { view.insert(1); } } } } } } // Write counts let mut words: Vec<&String> = map.keys().collect(); words.sort(); for &word in &words { if let Some(count) = map.get(word) { try!(writeln!(writer, "{}\t{}", count, word)); } } Ok(()) } 


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)

Rustyregex! \ wRegex \ wregex! [...]Regex [...]Nim
release, -i1x1.30x0.44x1.14x0.75x
release1.07x1.33x0.50x1.24x0.73x
debug, -i12.65x20.14x8.77x19.42x3.51x
debug12.41x20.09x8.84x19.33x3.25x


Notes:
  1. In the Rust regex! works faster than Regex , and r "[a-zA-Z0-9 _] + is faster than r" \ w + " . All 4 combinations were tested.
  2. The debug version is just for comparison.
  3. 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'; } } } /// Iterate to next state. Return false if the state remains unchanged. pub fn next(&mut self) -> bool { let mut newmap = [[false; MAP_WIDTH]; MAP_HEIGHT]; for i in 0..(MAP_HEIGHT) { for j in 0..(MAP_WIDTH) { let mut nlive = 0; for i2 in i.saturating_sub(1)..cmp::min(i+2, MAP_HEIGHT) { for j2 in j.saturating_sub(1)..cmp::min(j+2, MAP_WIDTH) { if self.map[i2][j2] && (i2 != i || j2 != j) { nlive += 1; } } } newmap[i][j] = match (self.map[i][j], nlive) { (true, 2) | (true, 3) => true, (true, _) => false, (false, 3) => true, (false, _) => false, }; } } // let changed = self.map != newmap; let changed = true; self.map = newmap; changed } } impl fmt::Display for Conway { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for row in self.map.iter() { for cell in row.iter() { try!(write!(f, "{}", if *cell { "()" } else { ". " })); } try!(write!(f, "\n")); } Ok(()) } } 


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:

  1. Comment out the sleep call in conway.nim and
    main.rs
  2. Change the number of loop iterations from 300 to 3000
  3. 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:

RustyNimNim / bc: onn = 30,000
(1) with map print1x1.75x1.87x1x = 3.33s
(2) without map print1x1.15x1.72x1x = 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:



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:

  1. Productivity: in the same time frame, you will overwrite more features in Nim
  2. Ease of learning
  3. Compiled language as a scripting language, good for prototyping, interactive research, batch processing, etc.
  4. Fishechki:
    • method redefinition
    • definition of new operators
    • named arguments and default values
    • powerful macros



Rust's strengths:

  1. This system programming language: embedded, without garbage collector, close to hardware
  2. Safe, disciplined, reliable
  3. Strong core team and active community
  4. 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.

Source: https://habr.com/ru/post/259993/


All Articles