I'm making a small program to benchmark Rust's performance compared to some other languages. The idea I came up with, was to take a file (with the numbers 1 to 10000000 written to it in separate lines) as a command line argument:
1
2
3
...
9999999
10000000
read the file, and then multiple every line in that file by two and write those back to the same file, so the end result would look like this:
2
4
6
...
19999998
20000000
My code works, and does exactly what it should do. I am especially looking for ways how I could make the code cleaner while also making it run faster. I'm currently running it with:
$ cargo run --release numbers.txt
The program:
use std::env;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Write;
fn main() {
let args: Vec<String> = env::args().collect();
let file_path = &args[1];
let mut file = OpenOptions::new().read(true).open(file_path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let vec: Vec<i32> = contents.lines()
.map(|line| line.trim().parse::<i32>().unwrap() * 2)
.collect();
let mut file = OpenOptions::new().write(true).open(file_path).unwrap();
for line in vec {
write!(file, "{}\n", line).unwrap();
}
}
1 Answer 1
This is my first post here, but I thought I might give this one a shot.
Your code could be made more user friendly by providing custom error messages.
- When you get the filename from the command line arguments, you assume that the user will provide at least one command line argument. You could provide a helpful error message if they don't.
- If opening a file fails for some reason (probably that the file doesn't exist at all), you could provide a custom print-out.
- My example uses
expect()
as an example, but in code where user-friendliness is actually a goal, you would probably want to useeprintln!("error message here")
andstd::process::exit(1)
to quit the program. That way, you don't get rust's extra panic messages which a user shouldn't have to understand.
You don't actually have to allocate a
Vec
to read the command line arguments, you can use the.nth
method on theArgs
iterator.You don't need the
trim()
call, because thelines()
iterator already removes the newlines.If you are looking to make the code cleaner, you could open the file for reading with
File::open
, and open it for writing withFile::create
.I don't know how to significantly improve the performance of your code.
- The biggest thing that I notice is that when you write the processed sequence of numbers to the file, you call
write!
on every line. This means that you talk to the OS every time you write a number. This could be made faster by using BufWriter, which would let you talk to the file only once. Alternatively, you could construct your output string while you are reading the file.
- The biggest thing that I notice is that when you write the processed sequence of numbers to the file, you call
use std::env;
use std::fs::File;
use std::io::{BufReader, BufRead};
use std::io::Write;
fn main() {
let file_path = &env::args().nth(1).expect("must provide an input file");
let file = File::open(file_path).expect("failed to open input file");
let doubled_contents: String = BufReader::new(file).lines()
.map(|line| {
let number = line.unwrap().parse::<i32>().expect("failed to parse file");
format!("{}\n", number*2)
})
.collect();
let mut output_file = File::create(file_path).expect("failed to open output file");
write!(output_file, "{}", doubled_contents).expect("could not write output");
}