Executable size and performance vs. C?

I just wrote a small Rust program to solve an Euler Project problem. We are talking about 29 lines of code. On an Arch Linux system, using Rust 1.6, the compiled executable, compiled with

rustc -O --crate-type bin

is 627KB.

The same program in C is 8.8 KB.

The C version runs in .155 seconds on a Thinkpad X-250. The Rust version is about 3x slower. Given the home-page marketing of "blazingly fast" and the chatter about a leaner runtime system, due to the lack of a GC, I'm more than a bit surprised that the executable is so large. It's almost as big as the executable generated by Gambit Scheme to solve the same problem. The execution speed is also a concern. It doesn't matter in this trivial case, but if that factor-of-3 were to apply to something substantial, it could be a show-stopper.

Comments?

1 Like

Could we please see the code?

fn amicable(n:i32) -> i32 {
    let dn=d(n);
    let ddn=d(dn);
    let mut result:i32=0;
    if (ddn==n) && (n != dn) {
        if n<100000 {
            result = result + n;
        }
    }
    result
}

fn d(n:i32) -> i32 {
    let mut result:i32 =1;
    for m in 2..(n/2+1) {
        if (n % m) == 0 {
            result = result+m;
        }
    }
    result
}

fn main () {
    let mut result:i32=0;
    for i in 1..10000 {
        result = result + amicable(i);
    }
    println!("{}", result);
}

Generally the efficiency of Rust is "close" to the efficiency of C code. What's the number for the Euler problem? I suggest to try paste again the Rust code here, because you have lost the formatting. You can also post the C code, for comparison.

Binary size usually boils down to dynamic vs static linking: Rust's standard library is included by default, C's is not.

(Also, I edited your second post so that it formats correctly)

3 Likes

This is your Rust code reformatted a bit:

fn d(n: i32) -> i32 {
    let mut result: i32 = 1;
    for m in 2 .. (n / 2 + 1) {
        if (n % m) == 0 {
            result += m;
        }
    }
    result
}

fn amicable(n: i32) -> i32 {
    let dn = d(n);
    let ddn = d(dn);
    let mut result: i32 = 0;
    if ddn == n && n != dn && n < 100_000 {
        result += n;
    }
    result
}

fn main() {
    let mut result: i32 = 0;
    for i in 1 .. 10_000 {
        result += amicable(i);
    }
    println!("{}", result);
}

I have also translated it to this similar C99 code:

#include <stdio.h>
#include <stdint.h>

int32_t d(const int32_t n) {
    int32_t result = 1;
    for (int32_t m = 2; m < n / 2 + 1; m++) {
        if ((n % m) == 0) {
            result += m;
        }
    }
    return result;
}

int32_t amicable(const int32_t n) {
    const int32_t dn = d(n);
    const int32_t ddn = d(dn);
    int32_t result = 0;
    if (ddn == n && n != dn && n < 100000) {
        result += n;
    }
    return result;
}

int main() {
    int32_t result = 0;
    for (int32_t i = 1; i < 10000; i++) {
        result += amicable(i);
    }
    printf("%d\n", result);
    return 0;
}

I compile the code with:
rustc -O -C prefer-dynamic test1.rs
gcc -Wall -Wextra -std=c99 -Ofast -flto -s test2.c -o test2

(More aggressive compilation switches don't improve the Rust code run-time).

Using:
gcc 4.9.1
rustc 1.8.0-nightly

My run-times are 0.20 seconds for the Rust code and 0.13 seconds for the C99 code.

On my Windows64 system the binary sizes are 49115 bytes for the Rust exe, and 15872 bytes for the C99 binary.

1 Like

Looks like Project Euler Problem 21.

I was curious about this, so I ran this code and got 0.18 seconds consistently. I then changed d() to use iterators:

fn d(n: i32) -> i32 {
    (2..(n / 2 + 1)).
        filter(|m| n % m == 0).
        fold(0, |result, i| result + i)
}

This version got me 0.13 consistently.

3 Likes

I tried your -C prefer-dynamic and it appears that Steve is correct -- the executable size is now the same as the C executable -- 8.8KB. Execution time is about .3 seconds, so now approx. a factor of 2 slower than the C version.

Is -C prefer-dynamic documented somewhere? I can't find it, but could have missed it. I do see discussion of the static linking in the book.

1 Like

If I use this:

#![feature(iter_arith)]

fn d(n: i32) -> i32 {
    (2 .. n / 2 + 1).filter(|m| n % m == 0).sum()
}

On my system it runs in 0.15 seconds, just a bit slower than the C version.

So why is the for-if version slower?

1 Like

Your runtimes and mine are apples and oranges. Different machines, possibly different OSes, libraries, etc. Compare to C on the same machine, compiled with clang, -O3.

Here's my C version:

#include <stdio.h>
#include <stdlib.h>

int d(int n) {
    int n_half=n/2, m, result=1;
    for (m=2; m<=n_half; m++)
        if ((n % m) == 0)
            result += m;
    return result;
}

int amicable(int n) {
    int dn=d(n), ddn=d(dn), result=0;
    if ((ddn == n) && (n != dn))
        if (n < 10000)
            result += n;
    return result;
}

int main () {
    int i, result=0;
    for (i=1; i<10000; i++) 
        result += amicable(i);
    printf("%d\n", result);
    exit(0);
}

Above I have compared both the Rust and C99 code on my PC.

Yes. My reply was to Steve's message.

Yes, you are correct.

@steveklabnik yep, was about to post the same thing

running 2 tests
test bench_iter ... bench:   1,339,438 ns/iter (+/- 11,603)
test bench_loop ... bench:   1,626,611 ns/iter (+/- 19,883)

I am not sure why, but iterators are often faster than loops when I compare.

Also the amicable method can be simplified to this:

pub fn amicable(n:i32) -> i32 {
    let dn = d(n);
    let ddn = d(dn);

    if ddn == n && n != dn && n < 100000 { n } else { 0 }
}

When iterators avoid you array bound tests, this is reasonable. But in this little program I don't know where the problem is (probably it's worth fixing).

Try the iterators version too :slight_smile:

That doesn't compile with 1.6:

dca@igor:~/Euler/rust$ rustc -O --crate-type bin problem_20.rs
problem_20.rs:14:3: 14:4 error: an inner attribute is not permitted in this context
problem_20.rs:14 #![feature(iter_arith)]
^
problem_20.rs:14:3: 14:4 help: place inner attribute at the top of the module or block
error: aborting due to previous error

So I did as suggested and got this:

dca@igor:~/Euler/rust$ rustc -O --crate-type bin problem_20.rs
problem_20.rs:1:1: 1:24 error: #[feature] may not be used on the stable release channel
problem_20.rs:1 #![feature(iter_arith)]
^~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error

Use:

.fold(0, |result, i| result + i)

.sum() and unstable features can't be used on stable compilers...

It's a rustc option, not a Cargo one. IIRC it's in rustc's man page, for Cargo, see