I came across some interesting benchmark results recently, and I figured I'd give a short writeup.
The benchmark is based on a subset of wc
's functionality - we wish to traverse
a directory, ignoring hidden files, and printing out the number of lines in each
file as well as a total.
Our ATS implementation is taken from the documentation, with a couple small changes to make it faster.
%{^
#ifdef ATS_MEMALLOC_GCBDW
#undef GC_H
#define GC_THREADS
#endif
%}
#include "prelude/DATS/filebas.dats"
#include "share/atspre_staload.hats"
#include "share/atspre_staload_libats_ML.hats"
#include "libats/libc/DATS/dirent.dats"
#include "libats/ML/DATS/filebas_dirent.dats"
#include "libats/DATS/athread_posix.dats"
#include "$PATSHOMELOCS/atscntrb-bucs320-divideconquerpar/mylibies.hats"
#include "$PATSHOMELOCS/atscntrb-bucs320-divideconquerpar/mydepies.hats"
#include "$PATSHOMELOCS/ats-linecount-0.2.3/lines.dats"
#staload $DivideConquer
#staload $DivideConquerPar
#staload FWS = $FWORKSHOP_chanlst
assume input_t0ype = string
assume output_t0ype = int
typedef fworkshop = $FWS.fworkshop
fun as_char(s : string) : charNZ =
$UN.ptr0_get(string2ptr(s))
fun dir_skipped(dir : string) : bool =
case+ dir of
| "." => true
| ".." => true
| _ when length(dir) > 0 => as_char(dir) = '.'
| _ => false
fun DirWalk(fws : fworkshop, fname : string, fopr : cfun(string, int)) :
int =
let
val () = $tempenver(fws)
val () = $tempenver(fopr)
implement DivideConquer$base_test<> (fname) =
(test_file_isdir(fname) = 0)
implement DivideConquer$base_solve<> (fname) =
fopr(fname)
implement DivideConquer$divide<> (dir) =
(let
var files = streamize_dirname_fname(dir)
var files = stream_vt_filter_cloptr<string>( files
, lam (x) => ~dir\_skipped(x)
)
var files = stream_vt_map_cloptr<string><string>( files
, lam (file) =>
string\_append3(dir, "/", file)
)
in
list0_of_list_vt(stream2list_vt(files))
end)
implement DivideConquer$conquer$combine<> (_, rs) =
(list0_foldleft<int><int>(rs, 0, lam (res, r) => res + r))
implement DivideConquerPar$fworkshop<> () =
FWORKSHOP_chanlst(fws)
in
DivideConquer$solve<>(fname)
end
#define NCPU 4
implement main0 (argc, argv) =
{
var fws = $FWS.fworkshopcreateexn()
var added = $FWS.fworkshopaddnworker(fws, NCPU)
var root = (if argc >= 2 then
argv[1]
else
".") : string
var nfile = DirWalk(fws, root, lam (fname) => let
var nline = wc_line(fname)
val _ = println!(fname, ": ", nline)
in
nline
end)
val () = println!("Total number of lines:\n ", nfile)
}
You can see the full code here; it is built with atspkg.
The equivalent Rust code:
extern crate ignore;
use ignore::WalkBuilder;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::fs::File;
use std::io::prelude::*;
use ignore::WalkState::*;
use std::sync::Arc;
use std::env::args;
fn main() {
let dir = match args().skip(1).next() {
Some(x) => x,
_ => ".".to_string(),
};
let acc = Arc::new(AtomicUsize::new(0));
let _ = WalkBuilder::new(dir)
.ignore(false)
.threads(4)
.git_global(false)
.git_ignore(false)
.git_exclude(false)
.parents(false)
.build_parallel()
.run(|| {
let acc_in = Arc::clone(&acc);
Box::new(move |result| {
let pp = result.unwrap();
let p = pp.path();
let file = File::open(&p);
match file {
Ok(mut f) => {
let mut contents = String::new();
let lines = match f.readtostring(&mut contents) {
Ok(_) => contents.lines().count(),
_ => 0,
};
println!("{}: {}", p.display(), lines);
acc_in.fetch_add(lines, Ordering::SeqCst);
}
_ => (),
};
Continue
})
});
let res = Arc::try_unwrap(acc).unwrap().into_inner();
println!("Total number of lines:\n {}", res);
}
And finally results:
Command | Lanuage | Time |
---|---|---|
awc |
ATS | 173.7 ms |
wc -l |
C | 291.4 ms |
rs-wc |
Rust | 76.76 ms |
As we can see, Rust remains queen of concurrency. ATS surpasses
wc
even with garbage collection, which is impressive, but it does not approach
Rust.