#please help optimize this src code

1 messages · Page 1 of 1 (latest)

fierce wren
quasi kayak
#

how are you compiling these?

fierce wren
#

cargo run --release
and
zig build-exe ./zig/main.zig -Doptimize=ReleaseFast

quasi kayak
#

Zig is almost 3x faster on my machine.

#

what's your version of zig?

fierce wren
#

0.13.0

quasi kayak
#

same.

vocal storm
#

-Doptimize is only for the build.zig command line, with build-exe and co it's -OReleaseFast

quasi kayak
#

cargo run is also impossible here, I assume there's some unchecked-in files

vocal storm
#

-D for build-* refers to defining C preprocessor defines for C source files

quasi kayak
#

actually my 3x is based on comparing full runs with hyperfine. Using the builtin timing, Zig is more like 38x faster

quasi kayak
#

optimize the Rust? Probably by making it use an array instead of a Vec

fierce wren
#

No, I want to optimize zig

quasi kayak
#

it's already 38x faster 🙂

#

the largest fib in this array only needs 68 bits of representation. Maybe you can use that.

minor lichen
#

how are you building the rust one it wasnt even close to that much slower for me

quasi kayak
#

rustc -O rust/main.rs

#

and comparing with the reported nanoseconds rather than just running the executables

minor lichen
#

yeah thats what i was doing

#

who knows

#

not like this really matters its such a small benchmark of something the compiler probably nearly optimizes away

quasi kayak
#

you'll be pleased to know that it's SIGNIFICANTLY slower with u68 instead of u128

#

yeah that's definitely a danger here with no output

vocal storm
#

the slowdown is likely due to the usage of vec

#

rather than using an array

#

if rust was using an array like zig is, I imagine it would be identical

quasi kayak
#
const LIMIT: usize = 100;

fn generate_fibonacci(fib: &mut [u128; LIMIT]) -> u128 {
    fib[0] = 0;
    fib[1] = 1;
    for i in 2..LIMIT {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
    fib[LIMIT-1]
}

fn main() {
    use std::time::Instant;
    let now = Instant::now();
    let res: u128;

    // Code block to measure.
    {
        let mut fib = [0u128; LIMIT];
        res = generate_fibonacci(&mut fib);
    }

    let elapsed = now.elapsed();
    println!("Rust: {} in {} nanoseconds.", res, elapsed.as_nanos());
}
#
Rust: 218922995834555169026 in 621 nanoseconds.
Zig: 218922995834555169026 in 1633 nanoseconds.

forgot to revert u68

#
Zig: 218922995834555169026 in 131 nanoseconds.

still faster but not as bad

#
const std = @import("std");
const stdout = std.io.getStdOut().writer();
const LIMIT = 100;

fn generateFibonacci(fib: []u128, n: usize) u128 {
    fib[0] = 0;
    fib[1] = 1;
    for (0..n - 2) |i| {
        fib[i + 2] = fib[i + 1] + fib[i];
    }
    return fib[LIMIT - 1];
}

fn function_to_benchmark() u128 {
    var fib: [LIMIT]u128 = undefined;
    return generateFibonacci(&fib, LIMIT);
}

pub fn main() !void {
    var t = std.time.Timer.start() catch unreachable;
    const res = function_to_benchmark();
    const elapsed = t.read();

    std.debug.print("Zig: {} in {} nanoseconds.\n", .{ res, elapsed });
}
#

so Vec was killing Rust, and Zig wasn't cheating by not doing the work

#

for optimizing it further, no idea really. It's too small of a problem to probably benefit from parallelism.

#

that version with a slice is quite a bit faster than a version taking a pointer to an array like my Rust version

#

if you want a more challenging benchmark and one that'll benefit from parallelism, try a mandelbrot generator.

#

Or you could compare tail-recursive fibonacci functions 🙂

#

tail-recursive performance is about the same actually.

#

maybe a little more illustrative of the languages:

const std = @import("std");

fn fib(n: u32, a: u32, b: u32) u32 {
    if (n == 0) return a;
    return @call(.always_tail, fib, .{ n - 1, b, a + b });
}

pub fn main() !void {
    var t = try std.time.Timer.start();
    const res = fib(47, 0, 1);
    const elapsed = t.read();

    std.debug.print("Zig: {} in {} nanoseconds.\n", .{ res, elapsed });
}

and

use tailcall::tailcall;

#[tailcall]
fn fib(n: u32, a: u32, b: u32) -> u32 {
    if n == 0 {
        a
    } else {
        fib(n - 1, b, a + b)
    }
}

fn main() {
    use std::time::Instant;
    let now = Instant::now();
    let res = fib(47, 0, 1);
    let elapsed = now.elapsed();
    println!("Rust: {} in {} nanoseconds.", res, elapsed.as_nanos());
}

You'll need to actually build that with cargo to get the 'tailcall' dependency.

vocal storm
#

yeah, I mean, not much further that you can optimize the fibonacci sequence - not exactly what I'd call a computationally intensive program lol

quasi kayak
#

these are all algorithmically optimized already, compared to a naive implementation

fierce wren
#

I thnk I have created exact replicas of both the src codes:

Rust:

use std::time::Instant;

const LIMIT: usize = 100;

fn generate_fibonacci() {
    let mut fib = [0u128; LIMIT];
    fib[0] = 0;
    fib[1] = 1;
    for i in 2..LIMIT {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
}

fn main() {
    let now = Instant::now();

    // Code block to measure.
    {
        generate_fibonacci();
    }

    let elapsed = now.elapsed();
    println!("Rust: in {} nanoseconds.", elapsed.as_nanos());
}

Zig:

const std = @import("std");

const LIMIT: usize = 100;

fn generate_fibonacci() void {
    var fib: [LIMIT]u128 = undefined;
    fib[0] = 0;
    fib[1] = 1;
    for (2..LIMIT) |i| {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
}

pub fn main() !void {
    var now = try std.time.Timer.start();

    // Code block to measure.
    {
        generate_fibonacci();
    }

    const elapsed = now.read();
    std.debug.print("Zig: in {} nanoseconds.", .{elapsed});
}

fierce wren
#

See, zig is 120 nanoseconds and rust is 91 nanoseconds

#

I am still thinking how is that possible, I had done all the R&D, I don't expect such results, am I doing something wrong here?

quasi kayak
#

you're running this in some cloud job, instead of locally where you can control powersaving and such?

fierce wren
quasi kayak
fierce wren
#

No, because some people distrust tests done on local machine

quasi kayak
#

OK, the reverse is much more sensible.

ancient plank
fierce wren
#

what is hyperfine?

quasi kayak
#

IMO the only way for these easy-to-see numbers to be worth anything, is to replicate hypefine more or less. Statistics, lots of runs.

ancient plank
quasi kayak
#

And still I just wouldn't trust <100ns differences at all. It's cloud.

ancient plank
#

wait a minute

#

couldnt the results of the functions just be optimized out since they arent returned?

#

also, why bother doing a microbench?

quasi kayak
#

anyway that's enough of my opinions. If the difference is meaningful and consistent my only guess would be that Zig and Rust aren't optimizing with the same CPU features in mind. Different detection of whatever this platform is.

ancient plank
#

only a limit of 100? thats tinsy-tiny

quasi kayak
#

fib(100) is already 68 bits long. You can't get big numbers with the function itself, without bignums and very different performance characteristics.

ancient plank
#

true, i didnt consider that

quasi kayak
#

you can also compare the assembly to see generate_fibonacci to see if there's something interesting about the optimization. Zig has quite a lot going on with the loop, it might have an optimization that works a lot better on my machine than on what's exposed to this random cloud job.

#

using the same CPU target, and some old one, like -mcpu baseline, might help for more similar builds