Live data from Hacker News

Should small Rust structs be passed by-copy or by-borrow? (2019)

forrestthewoods.com

61–70 of 238 posts

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#61

I don’t feel like this gave a satisfactory answer the question. Since everything was inlined, the argument passing convention made no difference in the micro benchmarks. But what happens when it does not inline? Then you would actually be testing by-borrow be by-copy instead of how good rust is at optimizing.

In Rust it's considered idiomatic to pass things by-value whenever you can. Usually this is also the most performant option, since it avoids dereferencing in the callee. Of course, if your struct is truly enormous, you may want to break this rule to avoid large copies. But in that case you probably want to Box the struct anyway. Of course, if your struct contains something that can't be copied--like a Vec --you'll ha…

I don't think I'd agree that idioms come into play here, one way or the other. Safely borrowing things by reference is one of Rust's headline features

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#62

Oh neat, that’s my blog. My old posts don’t resurface on HN that often. Lots of criticism of my methodology in the comments here. That’s fine. That post was more of a self nerd snipe that went way deeper than I expected. I hoped that my post would lead to a more definitive answer from some actual experts in the field. Unfortunately that never happened, afaik. Bummer.

Maybe it'll happen here! :)

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#63

I don’t feel like this gave a satisfactory answer the question. Since everything was inlined, the argument passing convention made no difference in the micro benchmarks. But what happens when it does not inline? Then you would actually be testing by-borrow be by-copy instead of how good rust is at optimizing.

I sort of agree and sort of disagree. > Then you would actually be testing by-borrow be by-copy instead of how good rust is at optimizing. I don’t think the question is actually: “what is faster in practice, a by-copy method call or a by-value method call”, I think the question is: “as an implementer, which semantics should I choose when I’m writing my function”. For the second question: “Rust is usually pretty good…

Blog author here. This feels like the best summary in this comment section.

The root question is indeed “what semantics should I use”. And the answer I came up with was “the compiler does a lot of magic so by-copy seems pretty good”. I agree with the previous commenter this is not a satisfying conclusion!

My experience with Rust is that it requires a moderate amount of trust in the compiler. Iterator code is another example where the compiler should produce near optimal code. Emphasis on should!

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#64
post #3

This is one advantage of Ada, where parameters are abstractly declared as "in" or "in out" or "out". The compiler can then decide how to best implement it for that specific size and architecture.

Dlang can also qualify parameters as in, out, and inout; although I don't know to what degree the compiler is able to use that for optimization purposes (it is used for safety checks IIRC)

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#65
I always prefer by-borrow. That's because in the future this struct may become non-copy and that means some unnecessary refactoring. My thinking is a bit like "don't take ownership if not needed" - the "not needed" part is the most important thing. Don't require things that are not needed.

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#66
post #40

This is one of the problems I have with writing rust code. You have to think about so many mundane details that you barely have time left to think about more important and more interesting things.

As a Rust beginner that likes to learn the hard way I think I have some insights why Rust seems cumbersome and/or hard for programmers trying it.

Rust uses syntax that feels familiar but means completely different things than in pretty much any other language.

For example '=' doesn't mean assign handle or copy. It by default means move.

'let' doesn't mean create a name for something. It means create physical space for something (of known size) that can be moved into or moved out of.

You don't deal with objects and values of primitive types. Instead everything in Rust is a value. When you move, you move the value. If you compare, you compare by value. If you pass something from variable into function, you move the value into the function.

And when the space where you keep the value goes out of scope value dies with it if it wasn't moved out to somewhere else.

Scope for variables (which are just named spaces for values) ends with the end of the block, but some values, created by functions and returned from them, if they are not moved into any named space, can die sooner, even in the middle of the line where they were acquired from function call.

Everything else stems from that fixed size moved value semantics. If you don't want to move the value into the function when you call it you need to pass something else instead, so you create and pass in the borrow. But you have to ensure that the value doesn't die or get moved anywhere (even inside the container you borrowed from) before borrows to it all die.

Because of this you are better off with borrows that are short lived and local. Often it's better to keep the index of an element of a Vec instead of the borrow of this element. If you must create types that contain borrows you must know that they become borrows themselves and you need to treat them exactly the same trying to limit their scope and life time.

It's hard when you come from any other language because borrows are superficialy similar to pointers or references to objects. So you try to use them as such. And crash into the compiler because they are not that. What's worse their syntax is very minimalistic which triggers intuition that they must be fast and optimal solution for many problems which they sure can be once you fully internalize their limitations but not a moment sooner.

Another thing is that values in Rust must have the fixed size. So even as simple thing as a string requires a bit of hackery. Basically in Rust the default strategy to have something of variable size is to allocate it on the heap and treat pointer to it (possibly with some other fixed sized data like length) as the fixed sized value you can move around clone and borrow.

So if you want to have semantics you know from other languages you can't just use basic Rust syntax.

You need constructs such as Box and Rc, Cell, RefCell. Make your things clonable and sometimes even copyable and avoid creating borrows whenever possible initially. When you do it Rust becomes as flexible as any other language and you can use it pretty much just as comfortably. Then the value semantics shines as you can very easily compare your data by value, order it, create operators for it, create has for it so you can keep it in HashMaps and HashSets. Then it's delightful.

My advice is when you create a long lived type just wrap it in Rc and treat this Rc as your 'object'. And avoid borrows in your types unless you have a very good performance (measured) reason to have them or you are creating something obviously dependant and usually short lived like an iterator.

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#67

I always prefer by-borrow. That's because in the future this struct may become non-copy and that means some unnecessary refactoring. My thinking is a bit like "don't take ownership if not needed" - the "not needed" part is the most important thing. Don't require things that are not needed.

Exactly, and if performance at some point matters: benchmark!

And I would bet 9 times out of 10 it won't be the bottleneck or even make a measurable difference.

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#68
post #40

This is one of the problems I have with writing rust code. You have to think about so many mundane details that you barely have time left to think about more important and more interesting things.

As a Rust beginner that likes to learn the hard way I think I have some insights why Rust seems cumbersome and/or hard for programmers trying it. Rust uses syntax that feels familiar but means completely different things than in pretty much any other language. For example '=' doesn't mean assign handle or copy. It by default means move. 'let' doesn't mean create a name for something. It means create physical space fo…

But Rc is only useful for creating tree-like data-structures.

One non-tree cross-link or back-link and you'll have to redesign your entire code.

Re: Should small Rust structs be passed by-copy or by-borrow? (2019)

#70
post #11

I would go with the version that gives the clean user interface (that is, by copy in this case). If it turns out that the other version is significantly more performant and this additional performance is critical for the end users consider adding the by-borrow option. The clarity of the code using a particular library is such an big (but often under-appreciated) benefit that I would heavily lean in this direction whe…

Agreed - and this applies in nearly every language: start simple, trust your compiler, and optimize only when performance becomes untenable.

This advice hinges hugely on what "start simple" really means. There's a ton of counter-examples here where that just isn't true at all depending on what you're calling "simple". In particular JIT'd languages can be especially problematic here. An example would be using Java's Streams interfaces to do something that could be done without much difficulty with a regular boring ol' for loop. At the end of the day you're hoping the JIT will eventually convert the streams version into the same bytecode the for loop version would have started with. But it won't do that consistently, and you've still wasted time before it did so.

Trusting the compiler also means knowing what the compiler actually understands & handles vs. what's a library-provided abstraction that's maybe too bloated for its own good and that quickly becomes "not simple" depending on your language of choice.

Post reply on HN