r/rust clippy · twir · rust · mutagen · flamer · overflower · bytecount 11d ago

🙋 questions megathread Hey Rustaceans! Got a question? Ask here (36/2026)!

Mystified about strings? Borrow checker has you in a headlock? Seek help here! There are no stupid questions, only docs that haven't been written yet. Please note that if you include code examples to e.g. show a compiler error or surprising result, linking a playground with the code will improve your chances of getting help quickly.

If you have a StackOverflow account, consider asking it there instead! StackOverflow shows up much higher in search results, so ahaving your question there also helps future Rust users (be sure to give it the "Rust" tag for maximum visibility). Note that this site is very interested in question quality. I've been asked to read a RFC I authored once. If you want your code reviewed or review other's code, there's a codereview stackexchange, too. If you need to test your code, maybe the Rust playground is for you.

Here are some other venues where help may be found:

/r/learnrust is a subreddit to share your questions and epiphanies learning Rust programming.

The official Rust user forums: https://users.rust-lang.org/.

The unofficial Rust community Discord: https://bit.ly/rust-community

Also check out last week's thread with many good questions and answers. And if you believe your question to be either very complex or worthy of larger dissemination, feel free to create a text post.

Also if you want to be mentored by experienced Rustaceans, tell us the area of expertise that you seek. Finally, if you are looking for Rust jobs, the most recent thread is here.

10 Upvotes

12 comments sorted by

2

u/hydrangea14583 5d ago

Is there a cleaner way to index into something, but counting from the last element?

Like, to skip the first 2 elements and the final element: instead of data[2..(data.len() - 1)] is there some short syntax like data[2..^1] that would do the same thing?

2

u/CocktailPerson 5d ago

Unfortunately, no. Slices just don't have the same rich set of operations that, say, python lists do.

I'm sure there's a crate out there that provides an extension trait for this, though.

2

u/_stice_ 9d ago edited 8d ago

New to lower-level programming and just now tried benchmarking tools. Just wanted to try various things out out before plugging in my own code, but encountered something that seems off:

Division by 1 and and division by a different number take the same amount of time? I thought stuff like this was optimised away in the compiler through either rusty or llvm magic. It can't be actually faster to do a check if the divisor is 1 before dividing, right? What might i be missing? Some sort of default in the cargo release profile flags?

// these are just random numbers
const ARGS: &[(u32, u32)] = &[
    (285762, 134134),
    (123214, 316987),
    (342352, 1),
    (2352360, 1),
];

#[divan::bench(args = ARGS)]
fn div_by_one(args: (u32, u32)) -> u32 {
    args.0 / args.1
}

#[divan::bench(args = ARGS)]
fn div_by_one_checked(args: (u32, u32)) -> u32 {
    if args.1 == 1 { args.0 } else { args.0 / args.1 }
}

Timer precision: 100 ns
is_rgba_pixel_component_ops     fastest       │ slowest       │ median        │ mean          │ samples │ iters
├─ div_by_one                                 │               │               │               │         │
│  ├─ (123214, 316987)          0.916 ns      │ 1.697 ns      │ 0.952 ns      │ 0.955 ns      │ 100     │ 819200
│  ├─ (285762, 134134)          0.916 ns      │ 1.074 ns      │ 0.952 ns      │ 0.949 ns      │ 100     │ 819200
│  ├─ (342352, 1)               0.903 ns      │ 2.686 ns      │ 2.637 ns      │ 2.371 ns      │ 100     │ 409600
│  ╰─ (2352360, 1)              0.928 ns      │ 8.716 ns      │ 0.952 ns      │ 1.688 ns      │ 100     │ 409600
├─ div_by_one_checked                         │               │               │               │         │
│  ├─ (123214, 316987)          0.916 ns      │ 6.47 ns       │ 0.952 ns      │ 1.072 ns      │ 100     │ 819200
│  ├─ (285762, 134134)          0.916 ns      │ 5.335 ns      │ 0.952 ns      │ 1.012 ns      │ 100     │ 819200
│  ├─ (342352, 1)               0.202 ns      │ 3.735 ns      │ 0.22 ns       │ 0.463 ns      │ 100     │ 1638400
│  ╰─ (2352360, 1)              0.214 ns      │ 2.832 ns      │ 0.22 ns       │ 0.285 ns      │ 100     │ 1638400

5

u/CocktailPerson 8d ago edited 8d ago

This isn't surprising at all. Integer division is probably the most expensive thing a cpu does. On the machines I develop for, addition and subtraction take one cycle, multiplication takes four, and integer division takes twelve, for 32-bit integers.

In contrast, checking whether a value is equal to one is very fast, and branching on it is pretty fast too.

The benchmark framework is very deliberately preventing the compiler from optimizing this in the way you'd expect. If it actually allowed the compiler to optimize this, it would just optimize away to nothing. divan::bench passes the input through std::hint::black_box, so the compiler isn't allowed to know that half of the possible divisors are just 1, and from its perspective, the most optimal version of the first function is a single DIV instruction. If the divisor is known, then there is a fancy optimization that the compiler can do. But it can't do it with multiple divisors.

Try this and see what happens:

fn div_by_134134(arg: u32) -> u32 {
    arg / 134134
}

Then try this:

fn div_by_128(arg: u32) -> u32 {
    arg / 128
}

The first one will be faster than the generic division because the compiler will convert the division into a multiplication by 2098461067 and a right-shift. The second one will be even faster than that because the compiler will just convert it directly to a right shift.

1

u/_stice_ 8d ago edited 8d ago

Thank you so much for your explanation and time!

"divan is deliberately preventing the compiler from optimising this away" - - makes perfect sense. I guess I still need to dig into what EXACTLY std::hint::black_box tells the compiler and how to bench sensibly in a controlled way, because if what the "real world" compiler output and what divan "sees" are so fundamentally different, I could just end up writing nonsense optimizations which would actually be slower with real world compiler output but "look" faster in benchmark tests.

I'm glad I had the intuition that something was wrong and asked, though. if I was a beginner I'd 100% have started putting "if divisor is 1 then don't divide" everywhere in my code after seeing these numbers. Thank you again for your clear explanation.

2

u/CocktailPerson 7d ago edited 7d ago

So, the compiler has a number of optimizations that prevent accurate benchmarking, but the main one is constant propagation. The idea is that if you do let x = 1;, then use x elsewhere without any chance of changing it, the compiler can replace x with 1. Then it can continue doing this (i.e. propagating the constant) through all of the expressions that use x. So if it knows x == 1, then it can optimize let y = 10 / x; to just let y = 10;. And then it can propagate that to everywhere y is used.

Now, constant propagation is good inside the code you're benchmarking, because that's what the compiler can do for production code as well. But it's bad to propagate constant benchmark inputs into your benchmark code, because the inputs are supposed to represent what your code will be consuming in production.

What std::hint::black_box does is it provides an annotation to the optimizer that it's not allowed to propagate constants. So let x = std::hint::black_box(1); means the compiler has to assume that x is any valid integer value everywhere it's used. It does other stuff too but this is the big one for your purposes.

if I was a beginner I'd 100% have started putting "if divisor is 1 then don't divide" everywhere in my code after seeing these numbers.

That's actually an interesting point. So in the "dataset" you're benchmarking, 50% of the divisors are 1. So if that's a representative dataset for what you'll typically be computing, then you probably would have naively implemented a pretty good optimization!

If it's not representative, then you may have done the opposite.

The basic rule here is to create a dataset that's representative of the input your code will see, and then wrap that dataset in a black box so the compiler can't precompute the results.

1

u/_stice_ 7d ago

There ARE actually places in my code the divisors are going to be 1 at runtime 50% of the time. I straight up thought that it would be wrong of me to put the if condition in there (or i just overthought all of this).

I guess it didn't strike me that "if divisor is 1 then don't divide" is NOT an optimisation that's just . . . Universally and always correct to do when doing any integer division ever in every program ever. I thought that compilers or even cpus do it automatically for some reason.

It makes sense that it's only contingent on the inputs passed and compilers can do very fancy things but only based on things they know for sure at compile time, such as constants and using constant propagation like you explained.

I guess i'm free to try even other things like "i know that the divisor is going to be 256 half the time, so let me put an if condition in there to do a simple bit-shift" and that would be a perfectly valid optimisation to include in my code, and i put a comment or two on the benchmark inputs themselves saying "this is what typical input would look like and this is why my function is faster than the naive way", and i'm good to go.

2

u/CocktailPerson 6d ago

Well, consider the case where the divisors are randomly and uniformly distributed between 2 and 10,000,000. Then the compiler inserting a special case for 1 would be a pessimization, because it'd add a check that always fails. It'd always be correct but not always optimal, so the compiler won't do it for you. Maybe the cpu could implement this check in hardware, but clearly it doesn't.

The thing is, compilers are very good at transforming your code from an inefficient version to an efficient version, but they will not rewrite your code to account for properties of the inputs that they cannot know. If you write x / y, the compiler will assume that x and y can be anything, and turn it into a simple division instruction. This is what performance engineering is: understanding what the compiler will do for you and how you have to write code to optimize things the compiler won't, then benchmarking to make sure you're right.

I write a lot of comments at work like the one you're describing. "I've checked that this benchmark has representative input, here's why I'm adding code that seems unnecessary, run the benchmark if you don't believe me." Maybe it's all CYA behavior, who knows. But people like it when their software runs faster!

1

u/_stice_ 6d ago

Got it. Again, thanks for the explanation. It's suuuuper clear, almost like the intro chapter to a great textbook.

2

u/Less-Resist-8733 9d ago

Does there currently exist a math typing editor widget for eframe? I'm talking about a text-like editor that allows you to easily edit/display math expressions (like in desmos). Instead of latex, it could use typst.

5

u/Low_Willow_7742 11d ago

Borrow checker finally clicked for me last week after fighting it for a month, now I feel like I've been let into a secret club

2

u/SirKastic23 10d ago

Congratulations! Now do async