5 ms·
Do C/C++ use such optimizations or is their type system too brittle?
by k__ 6y ago
Do C/C++ use such optimizations or is their type system too brittle?
- steveklabnik 6y agoI am a non-expert in the exact semantics of C and C++ layout, but as far as I can recall off the top of my head, they do not, because the general semantics are "lay out the struct in the order as declared, add padding for alignment reasons, done." Whereas the semantics in Rust are "we can do whatever we want unless you add an explicit #[repr] attribute, in which case you choose the semantics of layout and we don't mess with it." It's not about the strength of the type system, it's about history.
- k__ 6y agoI see, thanks. Pretty cool to see that Rust can do more optimizations than other languages. And it seems to play out well. I fondly remember Java being touted as being superior to C/C++ because it can do runtime optimizations, which will outperform C/C++. Somehow this never happened.
- MereInterest 6y agoSort of. You're right on what the abstract C++ machine does, but C++ also has the "as if" rule. A compiler can make whatever changes it wants so long as all observable effects are as if it had generated exactly the program as requested. This is the rule that allows for compiler optimizations to be done. Values stored in memory are not considered to be observable effects, so the compiler is allowed to make whatever changes it wants on that end. However, to the best of my understanding, most types are exposed to other compilation units, so it is hard to establish invariants about how they are used. Unless the compiler can verify that nowhere in the program ever takes the address of a boolean variable in a struct, it isn't allowed to rearrange the struct to avoid having that extra boolean. That might be possible at link-time for statically linked programs, but I'm not sure. Also, my knowledge is mostly from programming in C++ and watching CppCon talks, so I may be out of date from the latest optimization techniques.
- MaulingMonkey 6y agoTheoretically you could roll your own such optimizations manually in a C++ codebase by abusing partial specialization. That is, you could write: template < typename T > class optional< std::vector<T> > { // ... }; And write an implementation that (ab)uses knowledge of the exact layout of `std::vector<T>` to avoid an extra bool. This would of course be a breaking change to the ABI of optional<std::vector<T>> if implemented by std::optional, so compiler vendors tend to avoid such things. There is one stdlib example of this, but sadly it's more infamous than inspirational: std::vector<bool>. The standard relaxes several guarantees of std::vector<T> in the case of T == bool, to allow it to bit-pack the booleans instead of using sizeof(bool) - e.g. an entire byte - per element. Some of those relaxed guarantees make std::vector<T> unsuitable for interop with C style APIs taking pointers + lengths if T might be a bool. Worse still, many (most?) implementations don't even implement the space optimization those guarantees were relaxed for - a worst of both worlds situation. If you actually need such space optimizations, you'll reach for an alternative such as boost::dynamic_bitset which provides them consistently.
- steveklabnik 6y agoThis is a good point; given that it's automatic in Rust, I assumed that they parent was talking about it happening automatically, but you are right that you absolutely can do this manually, and it's good to have that nuance. After all, this PR required manual intervention to get the automatic parts to kick in!
- tjalfi 6y agoThat particular optimization is unlikely but there have been C compilers that optimized struct layout. 179.art, one of the SPEC2000 benchmarks, has some poorly laid out structs. Sun introduced targeted optimizations for this benchmark and several other vendors also did so. I have also read papers about profile-guided struct reordering but don't have a citation on hand. GCC also had a pass[0] for this optimization but it may have been removed. [0] https://www.research.ibm.com/haifa/dept/_svt/papers/golovanevsky.pdf https://www.research.ibm.com/haifa/dept/_svt/papers/golovane...
- lpapez 6y agoWhen it comes to std::optional, the answer is no. In fact, std::optional<T&> is explicitly forbidden (see here: https://www.fluentcpp.com/2018/10/05/pros-cons-optional-references/ https://www.fluentcpp.com/2018/10/05/pros-cons-optional-refe...) It is used in some other places though. For instance, std::string implementations typically employ small-string optimization (SSO) which means that there might be a limit on string::size() since some of the bits inside that value are used for different purposes (example: https://akrzemi1.wordpress.com/2014/04/14/common-optimizations/ https://akrzemi1.wordpress.com/2014/04/14/common-optimizatio...). The "niche" in this case is the realization that strings are unlikely to be exabytes in size, so there is no reason to always store zeros in high bits of _size.
- steveklabnik 6y agoFun trivia fact: Rust's stdlib String type cannot, and therefore does not, support SSO.
- drran 6y agoIf you need [no_std] version, use SmallString/SmallVec. Otherwise, use SmartString, SmolStr, etc.
- pdimitar 6y agoI recently wondered if a library like `smol_str` will be upstreamed one day. I feel people would rejoice if they can use stack-allocated strings (up to a certain size). Do you think that's possible?
- steveklabnik 6y agoAnything is possible, but it's not clear to me what advantage being in the standard library would actually bring. It is a pretty niche use-case (and I work in embedded these days), and it is pretty trivial to use a package. Only the libs team can really say, and I'm not on that team.
- 6y ago
- brundolf 6y agoThere are some cases, like the Option<&T>::None case, where C/C++ do roughly the same thing just without any type guards. So in this case: pointers in C/C++ can simply be null, which is not expressed at a type level but is lumped in as a special case of the pointer value itself instead of being a separate bit. i.e. the optimization in this case is only necessary for Rust because it normally represents the enum variant as a separate component of the value in addition to its contents. The optimization brings it back in line with C/C++ from a memory usage standpoint. However, because there are no type guards for this stuff in C/C++ you're generally constrained by convention/intuitiveness, when picking these special values, in order to (hopefully) guard against accidental misuse. So in the NonZero case above, you probably wouldn't do that because it's pretty unusual, and you'd therefore use more memory. And then, even when the "special values" are chosen "reasonably", they sometimes get misused anyway and we end up with bugs like last year's trivial sudo exploit: https://bit-sentinel.com/to-sudo-or-not-to-sudo-demystifying-cve-2019-14287/ https://bit-sentinel.com/to-sudo-or-not-to-sudo-demystifying... > Reading the manual for those functions we learn that -1 is a special value > Supplying a value of -1 for either the real or effective user ID forces the system to leave that ID unchanged. > If one of the arguments equals -1, the corresponding value is not changed. > So, what happens when we send -1 as a parameter? Well, somewhere along the underlying code lines is a convention that -1 is represented as 4294967295, thus either values will get the wanted result. Edit: In practice the above may be more common in C than modern C++