Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Book section: Tips for embedded C / C++ developers #126

Closed
japaric opened this issue Jul 17, 2018 · 12 comments
Closed

Book section: Tips for embedded C / C++ developers #126

japaric opened this issue Jul 17, 2018 · 12 comments

Comments

@japaric
Copy link
Member

japaric commented Jul 17, 2018

We agreed on having a section containing tips for embedded developers that have a C / C++ background. For example, they may be used to using for loop + indexing on arrays but it's more efficient to use iterators in Rust so they should use that instead.

Anything else that should be covered? cc @thejpster

@diwic
Copy link

diwic commented Jul 21, 2018

I wrote an article some time ago, about some Rust patterns that replaces C pointers. Might be helpful in this context too?

@korken89
Copy link
Contributor

One thing that is quite popular now is to use template meta-programming in C++ to configure hardware at compile time with guaranteed optimization. This is one part that kept me from coming to Rust for a long time, and I still have not found a way to do this well in Rust.

In C++ there is a continuous move towards constexpr functions, which (under certain requirements) are guaranteed to be evaluated at compile time. In Rust, it still seems to be dependent on the optimizer to evaluate an entire function, plus this causes issues that I have not been able to find good solutions for when generating data structures at compile time.
Granted that a lot can be done in build.rs and using include!, but finding good examples and how/when to use it was not trivial in my experience.

If we can answer these a bit I believe it would be good for convincing the embedded C++ (library) developer.

@therealprof
Copy link
Contributor

@korken89 No idea where you got that from. It's pretty much exactly the other way around: In Rust variables are const by default and everything is fully propagated through the compilation passes meaning that most functions will just turn into hot air leaving only few instructions behind. All automatically and per default without fiddling with functions trying to convince the compiler to that something is const. It's truly zero cost abstractions something that C++ never really achieved.

Mind you, the incredibly optimised binaries is something you will only get with release builds; debug builds are a bloat nightmare.

The problems are more on the opposite side of the spectrum: The safety guarantees of Rust make it rather cumbersome to share data, work with self-referential data structures or (especially in embedded areas) work with dynamic amounts of data.

@korken89
Copy link
Contributor

@therealprof

No idea where you got that from.

I got it from a lot of embedded development (libraries, OSes, and firmwares) in C++ together with participation in the embedded and metaprogramming C++ communities, and now rewriting my C++ libraries into Rust.

In Rust variables are const by default and everything is fully propagated through the compilation passes meaning that most functions will just turn into hot air leaving only few instructions behind.

I am aware of what you speak, but it is up to the user to make sure that a function will be evaluated at compile time. The simple example, is to give inputs that are runtime dependent, without realizing it, and the compiler will gladly generate runtime code for it rather than to generate a compile time error.

Moreover, the LLVM backend is not omnipotent, and assuming that the compiler will optimize everything away does not always happen (unless there is something new I have missed that guarantees this), there are some really good talks on YouTube by Chandler Carruth on the optimization LLVM does, which gives a good view into how well it will optimize and what the limits are.

It's truly zero cost abstractions something that C++ never really achieved.

Here you are misinformed, template metaprogramming (type based) and constexpr metaprogramming (value based) are both true zero-cost abstractions as they are performed at compile time (while granted that template metaprogramming generally needs O1 to fully make everything disappear in the binary). Sadly I do not have a curated list of true zero cost abstractions, but this presentation by Chandler Carruth gives some insight into what can be done to generate zero cost abstractions.

But indeed, there are some parts of C++ which truly are not zero cost.

Mind you, the incredibly optimised binaries is something you will only get with release builds; debug builds are a bloat nightmare.

Indeed, and this is a significant issue. One way to solve it in C++ is to use value based metaprogramming and a constexpr variable as a compression point, then only the data structure remains.
Another approach is to do debug build on O1 rather than O0 (from a C++ perspective), however this is a specific trick to C++.

The problems are more on the opposite side of the spectrum: The safety guarantees of Rust make it rather cumbersome to share data, work with self-referential data structures or (especially in embedded areas) work with dynamic amounts of data.

I am talking about compile time data structures in this case (stored in flash or used to initialize a run-time data structure), run-time data follows the normal Rust way and it is assumed that the readers have read the Rust book before coming here. Please see the Introduction chapter for the recommendations/assumptions given to the reader.


Back to the question at hand now, as coming from a C++ perspective these questions are still active and I still believe that it should be covered to some extent.

@therealprof
Copy link
Contributor

@korken89

I am aware of what you speak, but it is up to the user to make sure that a function will be evaluated at compile time.

In Rust? No. It's not even possible to ensure that. (Well, with the exception of const functions but they're only usable for initialisation of static data and often not even that).

It's truly zero cost abstractions something that C++ never really achieved.
Here you are misinformed,

No, not quite. I'm fully aware that this is a goal of C++ and can be achieved by expert programmers but is except for a few exceptions not done due to being extremely time consuming. Also with C++ that works only inside a single compilation unit or when excessively using code in header files (which a lot of other people, me included, frown upon), which is neither possible in Rust nor necessary.

Back to the question at hand now, as coming from a C++ perspective these questions are still active and I still believe that it should be covered to some extent.

I also have some C++ background (and consider myself an expert C programmer) and have experienced the exact opposite of what you're trying to say needs fixing/documentation. Proper zero cost abstractions in C++ require a tremendous amount of effort to implement and ecosystem support (which often simply does not exist) and just happen to work out of the box in Rust, which is one of the main reasons why I completely gave up on embedded C/C++ development.

I'd really like to learn more about the problems you're facing because I have a strong suspicion there's an easy fix to your problem and yes, that's something that should go in the book, too.

@Ixrec
Copy link

Ixrec commented Jul 21, 2018

The last few comments sound extremely confused to me, probably in part because const means two completely different things in C++ and Rust (maybe one or both of you already understand this, but: in C++ a const variable is only immutable, and may still be a runtime value; in Rust a const is always known at compile-time), and because Rust's compile-time evaluation is only partially implemented and a small subset of it is stable.

In both C++ and Rust, it is trivial to force the compiler to evaluate something at compile-time or error if it cannot; you simply declare a variable to be constexpr (in C++) or const (in Rust). You can do a lot more with this today in C++ only because C++'s constexpr functions have been available for a while, while in Rust defining your own const fns is not stable yet. Only the const-ness of some functions in std is stable. Once it's stable to define your own const fns, and the scope of what const fns can do is sufficiently expanded, pretty much everything C++'s constexpr can do should be doable in Rust too.

For actual template metaprogramming that const/constexpr doesn't cover, I suspect a lot of what C++ can do today is already possible in Rust (most famously, we have a compile-time interpreter for a variation of Brain****, which I guess is using "trait metaprogramming"?), and once we get const generics I suspect everything that's feasible in C++ will also be feasible in Rust.

@korken89 Does that answer your questions?

@korken89
Copy link
Contributor

@therealprof

Proper zero cost abstractions in C++ require a tremendous amount of effort to implement and ecosystem support

Indeed, and I have been creating these kind of abstrations through header-only libraries as you said.
This is also what has taken me towards Rust, as it is a great pain in C++ - too many ways to subtly fail and generate non-optimal assembler (which also is why Compiler Explorer is my main tool to evaluate the generated assembler in).

I'd really like to learn more about the problems you're facing because I have a strong suspicion there's an easy fix to your problem and yes, that's something that should go in the book, too.

@Ixrec really hit it on the nail here, thanks for the clarifications! The building block that I use in libraries for embedded system is constexpr to guarantee compile time evaluation - and guaranteeing compile time evaluation is really important when providing strong guarantees in libraries.
The second part is generating compile time tables and data structures, but I think using build.rs is sufficient here. Examples could include: generating CRCxx tables as there are a large amount of different CRC polynomials, generating twiddle factor tables for FFT implementations based on the input sizes and if real/imaginary inputs will be available.

I was not aware about const fn and const generics, the RFCs were a really good read! Knowing this, I would probably put a note for the C++ users to say how to get constant evaluation now (to the extent it allows with const) and what we can expect in the future to still the questions for now.

@therealprof
Copy link
Contributor

@korken89 You might want to have a look at macros, too, to generate tables and/or runtime expressions at compile time.

@korken89
Copy link
Contributor

@therealprof Thanks, I'm already there :) Trying to learn procedural macros now.

@Ekleog
Copy link

Ekleog commented Aug 2, 2018

IMO, how to use the build system for cross-compiling to embedded systems is the most important thing to put in this section, if it's not already somewhere else. At least that's what I had most issues with when switching to Rust for embedded development :) I guess that's going to be better with upstreaming of xargo into cargo, though.

@jamesmunns
Copy link
Member

@Ekleog check out #48

@japaric
Copy link
Member Author

japaric commented Aug 10, 2018

This issue was moved to rust-embedded/book#9

@japaric japaric closed this as completed Aug 10, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants