Benchmarking level generation: Go, Rust, Haskell and D
togototo.wordpress.com
togototo.wordpress.com
Here's one example that I picked up. The author writes that they had to refactor part of the Haskell code from (paraphrasing)
if (checkColl tr rsDone || checkBound tr)
then {- branch 1 -}
else {- branch 2 -}
into if checkBound tr
then noFit
else if checkColl tr rsDone
then noFit
else {- branch 2 -}
where
noFit = {- branch 1 -}
because of "problems with lazy evaluation". But in fact the only problem is that the call to `checkBound` is fast whereas the call to `checkColl` is slow, and the || operator evaluates its left argument before deciding if it needs to evaluate its right argument (just like in C, and every other language I've ever used). So all that is required to get the speedup is to switch the order of the calls: if (checkBound tr || checkColl tr rsDone)
then {- branch 1 -}
else {- branch 2 -} Rust 0.34
Rust isaac 0.57
Rust orig 0.70
Clang 0.42
GCC 0.36
My rewritten version: https://github.com/huonw/levgen-benchmarks/blob/master/R.rs (I didn't bother implementing seeding of the RNG for this, so it's the same result every time. Edit: implemented now, no performance change.)[1]: The new RNG is XorShiftRng, which is also in the standard library, and actually has far better randomness properties than most platforms' `rand()` functions anyway.
(Edit: merging two adjacent `if` statements, took it down to 0.34 from 0.36.)
edit: wrong addition left thereafter for posterity, ignore it (apparently Go uses ISAAC so the change to XorShiftRng might be unfair to Go (though it's trailing anyway). I expect C uses the standard libc generator?)
if((((rx + rw +1 ) < x) || ((rx > (x+w +1 ))))) { RoomOkay=true
} else if((((ry + rh +1 ) < y) || ((ry > (y+h +1 ))))) {RoomOkay=true
}else {RoomOkay=false}
if(RoomOkay==false){ return true}
Gaaaah, really? "go fmt" is your friend! That mess automatically cleans up into this: if ((rx + rw + 1) < x) || (rx > (x + w + 1)) {
RoomOkay = true
} else if ((ry + rh + 1) < y) || (ry > (y + h + 1)) {
RoomOkay = true
} else {
RoomOkay = false
}
if RoomOkay == false {
return true
}
> Particularly ugly is (*mySlice)[indexI].myField=newValue;
> I’m not sure why the language can’t automatically dereference the slice when the [] operator is applied to itI don't know exactly what the Go team would have to say about this, but my take would be language simplicity. Pointers are pointers, and special-casing some of them breaks the rules that both the compiler and the programmer rely on.
It may look like C, but it isn't. There's no pointer/array equivalence here, indexing and dereferencing aren't the same thing. If I weren't coming from a C background (which I'm guessing the author is, too), I'm not sure it would ever occur to me to expect a pointer-to-slice/array to be something you could index.
Also, as a slice is basically a pointer to array plus two ints, you normally wouldn't pass a pointer to it in the first place. You only need to do that if you want to resize it.
First, thanks for the feedback!
Unfortunately, I don't think this feature will be added to Rust, though. IMO it causes bugs, as accidentally failing to initialize a variable yields a garbage value at runtime. There's no reason that 0 is any more likely to be a valid integer value than 1, or 42, or 0xdeadbeef, and the compiler shouldn't try to guess. And the feature wouldn't work in Rust anyhow due to the lack of null pointers.
However, having an explicit zero value is convenient, and this is what the Zero trait is for. With Rust traits you can overload on the return value, like Haskell, so you can write:
let (emptyr: Room, emptyt: Tile, emptyl: Lev) = Zero::zero();
let mut ls = [ emptyl, ..100 ];
assuming that your types derive Zero (which you can do with #[deriving(Zero)]). In the future it should be: let (emptyr, emptyt, emptyl) = zero();
let mut ls = [ emptyl, ..100 ];
This could be done by either writing a zero() wrapper in the prelude or allowing static methods to be imported, both of which are probably good ideas. rusti> let (a,b,c): (int, uint, float) = std::num::Zero::zero();
rusti> a
0
rusti> b
0
rusti> c
0
(The explicit type annotations of the tuple are purely because of how rusti works, in most cases they can be dropped as they will be inferred from the use of a, b and c.)When a language default is to zero-initialize, it creates a strong cultural impetus within that language's user community to define data structures such that zero initialization is a valid state.
But if the language is taking a hard line against null (rather than e.g. being able to annotate locations as null-accepting as required, or vice versa), zero initialization may not be the right choice. However the combination of a requirement for definite assignment and non-nullability tends to make algorithms involving dynamically allocated arrays awkward to implement.
We need to stop just looking at numbers and investigate what these compilers allow in terms of productivity, optimization opportunities and explicit control of generated code.
If you look at x264 numbers you'll see that there is up to a x10 gap between hand-optimized assembly and native C, and I believe that this gap exist in most fast programs because optimizing backends are too much black-boxes that consistently produce quite good, even sometimes amazing results, but with little control or feedback.
The nature of highly optimized code is detailed, explicit control of what should happen. I will take a poorly optimizing compiler any day if it allows me complete control of the backend optimizations it chooses, if I can give him register hints that actually matter, if it stop compiling and ask for the annotations he need to optimize better. C++ compilers are good at this game but this could be taken further.
Also it's unfair to never look at language productivity at the same time as speed, since the ability to produce fast programs will be notably affected by the speed at which we can achieve that anyway.
I am not sure I like the sound of that. I guess having the unsafe stuff available is very useful for certain things, but it all sounds a bit messy, too.
Reasoning about mutability is necessary for safe (and fast!) concurrency.
Technically I believe (to verify) Go does that as well, but the mix is implicit and the system (not the developer) will decide on its own whether a value should be stack- or heap-allocated.
C# will also allocate value types local variables on the stack (technically all local variables are stack-allocated, but for reference types it obviously stack-allocates a reference to the heap, the end-result is that under the usual understanding of "stack allocation" only value type local variables and parameters are stack-allocated)
So depending on how you count, that's either one or three pointer types, which I'm happy with. The "unsafe" pointers exist only for C interop and have no bearing on normal code, so it's inaccurate to count them.
That leaves ~ and & which you don't normally need to think about, you could get pretty far writing ~ for all allocations and & in all fn signatures. You could write a video game like this with the SFML bindings for example.
mut doesn't really complicate things in terms of which pointer you're picking. I've screwed up mut here and there, but I feel like it's its own subject.
Edit: And then of course actually use the prominent guidelines for good-looking code in each language!
> sidenote: Your blog's code formatting is awful.
fyi - it is not my blog. Just thought it was a neat post.- make use of functions like `vec::from_fn`, `uint::range` and `some_number.times`, rather than writing `while`-loops with a counter explicitly.
- use iterators[2,3] for traversing data structures, since it allows the data structure to optimise the accesses (e.g. the vector iterator avoids bound checks).
However, all the tutorial/manual are out of date, and the documentation makes it very hard to find out about these functions; so it's definitely not the programmers fault when they turn out non-idiomatic Rust.
[1]: https://github.com/mozilla/rust/wiki/Note-style-guide [2]: http://static.rust-lang.org/doc/tutorial-container.html#iter... [3]: http://static.rust-lang.org/doc/core/iterator.html
- https://github.com/tibbe/haskell-style-guide/blob/master/has...
- http://stackoverflow.com/questions/6398996/good-haskell-sour...
Or just change `(checkColl tr rsDone || checkBound tr)` to `(checkBound tr || checkColl tr rsDone)`.
With D it is not a bad idea to use dmd for development and GDC/LDC for release builds.
Regardless what one thinks of Haskell, it's a testament to the work of the GHC developers.
Still, it looks mostly like a test of the random number source. And also none of the nice high level optimizations are firing anyway, http://www.reddit.com/r/programming/comments/1ixnf6/benchmar...
To get a fair comparison without completely changing the benchmark, you should probably subtract .3-.4 seconds from each runtime as that's the amount of time spent in random number generation (again presumably all languages use libc's).
Indeed it is reimplemented: http://golang.org/src/pkg/math/rand/rng.go
Rust uses a cryptographically secure random number generator (ISAAC) by default, to try to protect users from themselves. This means that the RNG is going to be slower than non-cryptographically-secure random number generators. You can get a non-cryptographic random number generator, but you have to ask for it.
Salsa20 is parallelizable and can skip ahead arbitrarily, has smaller state, and is fast: on recent processors the amortized time per 32-bit integer is around 8 cycles. It was also thoroughly vetted as an eSTREAM cipher.
I don't know if Rust has intrinsics or some other kind of vector register support. I'd even volunteer to implement Salsa20 on it.
[1] http://bench.cr.yp.to/supercop.html
[2] https://github.com/floodyberry/supercop/blob/master/crypto_s...
[3] https://github.com/floodyberry/supercop/tree/master/crypto_s...
http://dlang.org/phobos/std_random.html
Of course, a better random number generator takes longer to compute. The blog's author switched the D version to use libc's random number generator, and got some significant speedups.
http://www.reddit.com/r/programming/comments/1ixnf6/benchmar...
http://hackage.haskell.org/packages/archive/random/1.0.1.1/d...
Go's implementation is here and doesn't use the libc generator either:
http://golang.org/src/pkg/math/rand/rng.go
Walter Bright has already commented on D.
it's mostly a test of random number generation and presumably all of those languages use libc's random number generator
In fact, that would be better than benchmarking different PRNG methods, as it's done now ;).
Anyway, I think the benchmark is still fair, since the programs use whatever the default implementation for a PRNG is in the language's standard library. Sure, the Haskell program will be faster if you'd usea MWC or MT implementation from Hackage. But you'd expect a standard library to come with a sensible default ;).
I'm hoping Rust's numbers improve. It's incredibly promising, but without C++ level performance, it's too much semantic complexity.
I'm sure these numbers will improve.
It is close, but Java is just as close, too. Not a tremendous performance advantage that someone would choose one over another for and not something I'd expect from a language marketed as "performance oriented".
The difference in performance between the languages is almost entirely due to differences in the implementation of the benchmark.
Rust has C++ level performance if you compare the same code in both languages.
I think nimrod is a very good contender and it's sad never seeing it being benchmarked when Go, D and Rust are.
I will gladly help you write a benchmark if you wish to pursue this .