Rust Functions Are Weird (But Be Glad)

preview_player
Показать описание
Rust takes a unique approach to function types, for both closures and fn items. In this video we'll talk about a way to fit these strange function types into your existing understanding of what types are. Then we'll look at how another language (okay, it's C++) does function types in a way that causes poor codegen in generic higher-order functions if you aren't careful--and how/why Rust avoids this problem.

Godbolt code samples from the video:

*Rust Stuff*

*C++ Stuff*

I use the amazing Manim library for animating these videos, and I edit them with Blender and Audacity.
Рекомендации по теме
Комментарии
Автор

The noinline thing in the C++ code* has caused some confusion, so let me clarify: I used noinline to simulate an example of a higher order function that is not inlined, to show how function pointer types hurt codegen (and lambdas help it) in template instantiations that aren't fully inlined. Template != "definitely inlined"; not being inlined is common for e.g. recursive higher order functions, or doozies like std::sort. While answering comments about this I came up with a clearer example, that doesn't use noinline, where in a reasonable implementation of a commonplace algorithm (in-order binary tree traversal), using a function by name generates worse code than using a lambda:


The lambda line generates zero code whatsoever (since due to the lambda type it instantiates walk() in a way that is statically known to dispatch to f(), which does nothing, so the whole thing optimizes away), whereas the function pointer line generates an out-of-line instantiation of walk() containing indirect calls.


I think this example is pretty compelling and if I could replace the one I used in the video with this instead, I definitely might. Even though Rust's codegen isn't a dramatic "home run" over C++'s since rustc doesn't optimize out the pointless recursion, it still supports my argument that Rust's type system lends itself to good codegen in non-inlined higher order functions, whereas C++'s type system works against it.

*I also disabled inlining of calculate() in the Rust code, for the record.

_noisecode
Автор

Another great video, thank you Logan!

NoBoilerplate
Автор

Rust: same function copy pasted? Different type.
Javascript: null is an object? Sure.

OrbitalCookie
Автор

regarding rustc merging functions: function merging is done by LLVM and clang can do it for C++ too, it's just off by default. you can manually turn it on with `-Xclang -fmerge-functions`

HMPerson
Автор

In C++ you can pass functions as template parameters using template<auto F> and then the compiler can optimize it properly. You can think of this as passing the function by value, just as a template parameter instead of a function parameter.

N....
Автор

Also if you write:

pub fn f_u32(num: u32) -> u32 { num + num }
pub fn f_i32(num: i32) -> i32 { num + num }


they will also be optimized to be the same function, because of the clever design of two's compliment

blt_r
Автор

So THAT's why lambdas can only be assigned to auto-typed variables. The reference is so jargon-heavy, I just couldn't figure out what a "unique unnamed non-union non-aggregate non-structural class type" even is.

protodot
Автор

ooooh. wow. I would have never thought of that. in other words: rust's functions allow for static dispatch. this enables the compiler to do much smarter optimizations since it knows exactly what function is being passed as an argument. brilliant!

henrycgs
Автор

how u have such deep understanding of such topics is just amazing, hope one would also able to get simple topics to such great depths

blouse_man
Автор

I'm not sure if this is within the scope of this channel, but making a video on the difference between hardware threads and software threads and how rust affects and is affected by that difference would be really interesting!

yoshiyahoo
Автор

I already knew Rust's particular "weird" function type approach—but I only already knew it because of those excellent, educational error messages! I'm sharing this with all my Rust students, because it explains it all much better than I have been.

SolraBizna
Автор

Great video! In this little series you started recently, you talk about things that I already (mostly) know. However you show implications much deeper that I would initially find out myself, which makes me rethink about those subjects in a different setting. For example, in the case of your previous video, I knew that returning &Option<T> is a bad idea and more idiomatic way would be to return Option<&T> instead. But I did not know all of the reasons *why* that is the case. That is all to say that I love your videos and hope to learn much more from you. Cheers!

aleksanderkrauze
Автор

This guy has very quickly become the subscription that I am most looking forward to his next video

mattshnoop
Автор

You can get 2 lines of assembly, with moving 20 to eax in C++ with normal function if both funcitons (f and tempalte) are defined as constexpr.

AzuxDario
Автор

6:05 I think the best way to think about closures are as tuples, or perhaps tuple + fn-pointer. With the tuple being your captured variables. When getting annoying problems with the borrow-checker, etc. it helps me understand to think of closures as tuples instead.

A closure like `|x: i32| x + y` would be the tuple (i32, ) since it captures `y` which is an i32. A closure which captures nothing is the empty tuple () which is zero sized, just like a normal function, so it can be coerced to one. Any other tuple is non-zero sized so needs additional stack space in addition to any potential function pointer making them incompatible.

BlackMsh
Автор

Your last point is very dubious (to not say misinformed). C++ does function merging (or "identical COMDAT folding") but it does so at link time, not compile time.
The reason is that with Rust, when you compile your file, it's considered a complete crate (AFAICT), while C++ consider is just an object file and doesn't do any things that could break the public ABI (since it can't know how the code will be used, unlike the linker).

minirop
Автор

These rust videos are wonderful. I've barely written any c++ code at all, but it's wonderful to look at how these immensely different approaches to similar-ish languages pan out at compile time.

mikkelens
Автор

I still don't get why it is necessary for functions, but not for e.g. integers. Wouldn't it be so great and efficient if the numeral "2" had the type 2? And there was a trait i32 that 2 would implement and then every computation on literals would be done using the type system? And arguments from the outer world would be "dyn i32"?
I feel just like this using the functions in Rust. I think I get that the advantages of using functions like that is far greater than the advantage of using "i32 as trait" approach, and this is the main argument for it. But it makes the whole thing unnatural for me, I would prefer consistency in the type system, the optimizations could be done behind the scenes, not at the expense of design.

mzg
Автор

I still think higher kinded types would be useful in rust if it could still be performant and safe. Having types based on types signatures. There is a world where having the same signature being the same "type" is very useful. Like defining a functor, monad etc... I know rust is moving towards Generic Associated Types, that helps solve some of these issue's. But, in my mind this function type you are mentioning is just a function ptr more or less. Still an interesting video, thanks!

tenthlegionstudios
Автор

Analyzing the compiled code is such a great way to learn about this

licksvr