That thread brings up per-container memory allocators, and mentions Vec. What's going on there?
I have been trying to find a good way to deal with hybrid memory/disk structures. One example is external sort, but there are a whole class of such structures used for databases (Hash Join, Hash Aggregation, etc.). The idea is to constrain the memory footprint of a structure and efficiently use the disk.
One challenge is simlly knowing how much memory is being used. Another is supporting multiple heaps/arenas/whatever and allocating in the right one. And a third is safely clearing parts of the structure that have been written to disk for later processing (I guess calling the destructors if necessary?).
Wouldn't memory mapping take care of a lot of these scenarios? The OS will do all the caching.
If you make a data structure that is meant to use one contiguous chunk of memory and access memory linearly and/or with locality when possible, I would think it would make explicit disk/hybrid scenarios much less necessary.
Consider a hash table. Access is basically random, so if 25% fits in memory and 75% is paged-out at any given time, then 75% of the lookups will be a page fault and require a page-in. Very costly.
Compare that to a Grace HashJoin (https://en.m.wikipedia.org/wiki/Hash_join) which partitions both sides (lookup side and hash table side) into matching partition pairs. The partitioning step and reading the partitions from disk are both sequential access. Then, pick up and process one pair of partitions at a time, which will only require a small hash table in memory.
Note that this is still way more efficient even when on an SSD, because an SSD is still block-oriented, and paging entire blocks in/out for a single lookup is still inefficient.
It even helps when the data fits into memory sometimes, because you can size the partitions small enough to fit in cache, which will make lookups even faster.
You can say "don't use hash tables", but that's not a great amswer for a low-level high-performance language like Rust.
I would never say "don't use hash table" but I do think that a lot of the benefits from special data structures like these are more due to the organization of memory than being explicit about memory loading and disk writing.
In this case it seems like the downsides you mentioned are the downsides of keeping things on disk, not memory mapping (which would only hit the disk on the first load and would automatically use any available memory). Organization to use disk pages effectively would benefit memory mapping as well.
But to organize memory you need some idea of how much you are using (e.g. how big the in-memory hash table is), and be able to free it in a reasonable pattern (e.g. deallocate the whole heap/arena at once). Both things are difficult in rust currently, and memory mapping doesn't help with either.
Memory mapping serves a similar purpose as temp files: a way to tell the OS that it can freely page that out to avoid uncontrolled swapping. But you need to do the organization yourself to keep that access sequential, which is hard to do in rust for the reasons that I mentioned.
If you memory map a hash table and have memory pressure, you'll go into the crazy bad scenario of a page-in for every lookup, which is avoidable with better organization.
I think we are basically in agreement but perhaps my point was not clear.
> But to organize memory you need some idea of how much you are using
When I talk about organizing the data structure I'm talking about how the data is layed out in the memory map and how locality is affected.
What I'm saying is not that I think all uses are covered by memory mapping, just that data structure layout combined with memory mapping can probably take care of lots of the majority of scenarios.
I did not know that organizing memory layout was difficult in Rust since it is very straight forward in C++.
(1) knowing how much memory a particular structure (like a hash table) is using
(2) managing that memory as a single chunk; e.g. to free the whole thing at once so that you can pick up another chunk that you had previously serialized
It's not impossible in rust, it's just that you have to build up all the pieces and none of it will integrate nicely with the rest of the rust code.
Box and Vec are starting to gain the ability to be parameterized over an allocator.
> Is there any work in this area?
Kind of, but it's slow going. You can support multiple heaps/arenas/whatever, but it means you have to implement your own. As the ecosystem standardizes around the allocator traits (only the global one is stable yet), there will be easier interop.
> That thread brings up per-container memory allocators, and mentions Vec. What's going on there?
I believe they're talking about custom allocator support for Box [0, 1]. The author of the PR says that they intend to add per-container allocator support once support for Box lands, starting with Vec [2].
He's talking about variable length arrays, which would be an unsized type in Rust and thus can't be stack allocated. For example, the following is valid C99:
`void foo(int n) { int values[n]; }`
I don't know that that's a bad thing. AFAIK the implementation details are underspecified even in C and compilers kind of do whatever they want (i.e. I've seen some embedded compilers that monomorphize it in multiples of 2 up to a limit and just hope the user doesn't exhaust the stack).
Edit: I'm pretty sure you can create a hacky version using iterators and a recursive function: `fn vla<T>(iter: impl Iter<Item=T>, init: T, index: usize, count: usize) { ... }`
Edit2: Actually he's talking about `alloca` which is a whole other "here be dragons" feature. See the reddit thread steveklabnik linked for some of the pitfalls
As a side note RFC#1909 is a accepted RFC which introduces unsized rvalues to rust allowing VLA's.
BUT:
- it's not implemented (maybe partially? idk.)
- it has unclear/non-trivial interactions with async-await transformation, ~mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case~ Edit: It turns out it's actually not really possible to use unsized values in generator which life across await/yield boundaries, as far as I can tell.
So it's one of the "it's not hear yet" situations.
Sadly, after trying specialization and obsessing over GATs threads/issues, I've learned not to get my hopes up about such complex features with so many subtle implications landing in stable any time soon :( - I've still got PTSD from all the compiler panics.
> - it has unclear/non-trivial interactions with async-await transformation, mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case.
This especially gives me pause. I've thoroughly enjoyed the async/await experience in Rust despite its rough edges but it seems the community's exploration of async/await is still in its early stages. It sounds that something as complex unsized rvalues will take a long while to get right (I don't know what I'm talking about though, so please correct me!)
Yes, also just after posting I noticed you can't have unsized values living across await in generators without some major changes or something.
The problem is that when you create a generator it doesn't yet know the unsized field it only knows it when creating it in a `poll` call. But it only will call poll after pinning the generator at which point you can't really grow the size of it for a bunch of reasons including that growing might move it in memory (realloc) which you can't due to self references...
So you would need the size of the generator when generating it.
Which could somewhat be possible but not really with current rust features, i.e. it would need a lot a special care.
When comparing rust and C++, I think it’s unfair to interpret “C++” as a specific standard, because you can’t do the same with rust. So, you would have to compare implementations.
AFAIK, in practice, many C++ compilers support alloca for compatibility with gcc (or more specifically, its C library)
“The function was present on Unix systems as early as 32/V (1978), but is not part of Standard C or any POSIX standard.”
I also don’t see how it could be part of the standard, as that, AFAIK, doesn’t even specify the existence of a call stack (a compiler is free to allocate call frames on the heap, for example)
Reading https://www.gnu.org/software/gnulib/manual/html_node/alloca...., I think gcc C++ supports alloca on most systems (“There are a few systems where this is not possible: HP-UX systems, and some other platforms when the C++ compiler is used. On these platforms the alloca module provides a malloc based emulation”)
C has standardized dynamically sized automatic arrays which is close enough to alloca. The standard wouldn't concern itself of were the space is actually allocated, just mandate that it is freed when the function terminates.
I've found just applying SSO to the container sufficient. Stack if it's small enough, heap if it isn't. You usually want a limit on your alloca anyway.
> Edit2: Actually he's talking about `alloca` which is a whole other "here be dragons" feature. See the reddit thread steveklabnik linked for some of the pitfalls
AFAIK VLA is already that, didn't Linux de-VLA the kernel a few years back because they were a common security issue and the code they generate is bad / slow?
> Secondly, the killing feature of C++ is that it is C. If you don't want to use exceptions or RTTI, then you can just switch the features off. Most of C programs can be just compiled with a C++ compiler with very small changes or without any changes at all.
If your C code happens to be compileable with a C++ compiler, then either your program is very small or you are writing really shitty C.
Idk about "shitty C" but indeed C++ being a super set of C is a common misconception, followed by the misconception that it's nearly a superset of C. ;=)
Simplest example is that a `void func()` decl in C and C++ have different semantic meanings.
I think "nearly a superset" is correct, the empty argument list doesn't really make enough of a difference to really make it a completely different language. I also don't think I've ever seen this "feature" of C used productively in modern codebases, except maybe to declare generic function pointers in something like dlfunc. In general it's just that people are too lazy to type `void func(void)` (or don't know the difference).
You can also tell that the C++ standard committee agrees with this to some extent since they tend to add C novelties into the C++ standards whenever possible to make it easier to write compatible code, for instance: https://en.wikipedia.org/wiki/C%2B%2B11#Improved_C_compatibi...
No arrays, no real strings. Pushing UB issues on the programmer instead of forcing the compiler maintainers to deal. No closures. Threading as a first class construct. No modules. Standard Library is increasingly a bad joke.
What's happened is the direct opposite of C++ where every fad gets added to the language. With C lots of standard proven features don't make it into the language.
I agree that teaching C++ as "C and then some stuff on top" is probably a bad idea. But I also think that it's not technically incorrect to say that "C is mostly a subset of C++", at least as far as most C constructs will work in C++ with only minor tweaks.
It's a bit like saying that operating the windscreen wipers is a subset of knowing how to drive a car. It's strictly correct, but it's probably not where you should start nor where you should put most of your attention if you're looking to get you license.
Why's that? C has a few features that C++ lacks, but it's possible to get by fine without using any of them.
The Lua interpreter is written in a subset of C so that it also compiles fine as C++. Or, if you prefer, in a subset of C++ so that it compiles fine as C.
What type errors? Do you mean that, to get it to compile as C++, you have to add an explicit cast? That isn't a significant burden.
Requiring explicit casts, and forbidding implicit conversions, reduces the odds of being surprised by an unwanted conversion. There's a good argument to be made for adding explicit casts to C code even when the language doesn't require them. GCC's -Wconversion flag can help with this.
There's very little harm in making the conversion explicit. It's more verbose, but it's also more explicit, which is a good thing.
There is harm in implicit conversions catching the programmer unawares. This is a fairly significant cause of bugs in C code, and it's why C++ uses stricter rules, requiring you to always make these conversions explicit, even when using malloc.
You're not entirely wrong, but in the context of the specific example given (malloc), it's about as implicit as c++'s auto keyword, it just obscures type information on the right side instead of the left side of the assignment from the programmer.
I would also argue that there's a hard to quantify harm in making code more verbose. More visual noise makes it harder to parse what's going on. Sure, one redundant cast isn't going to kill you. But many sources of repetition across an entire code base eventually turn it into a slog where the parts you want are obscured by useless noise.
> What type errors? Do you mean that, to get it to compile as C++, you have to add an explicit cast?
Yes.
> That isn't a significant burden.
But once you do it your "C" code is littered with a lot of unnecessary casts that increase the verbosity of the code and could hide errors. With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
How? As I said, the stricter rules of C++ result in fewer type-conversion errors than C's permissive rules. That's the reason for C++'s stricter rules, and it's the reason Ada is even stricter.
As an example, consider
a = b = c;
In a languages with permissive rules about implicit conversions, you have to inspect the variables' types to determine whether this statement is equivalent to
b = c;
a = c;
(Even ignoring OOP, that is.)
> With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
Wouldn't it just fail to compile if you're missing a required header?
edit steveklabnik linked to [0] so I see what you mean now. I think that's a valid point.
It definitely isn't. C99 and even some C11 features used all over the place. Designated initializers are heavily used. We'll see when those actually end up stable for C++20.
Even then, I never use the latest standards of C++, I stay a couple standards behind so I can rely on whatever compiler I pick being able to handle it. Right now I'm using C++14.
I see what you mean but I think the point the article is making here is true: you can effectively strip most of the features of C++ and end up with something with a footprint and runtime cost almost identical to C. You can effectively port almost any C program to C++ by making purely syntactical changes that won't have any impact on the execution.
Well, we didn't mention this in the article, but this is very practical actually. About 10 years ago we had a request significant reworking an open source DNS server and the main problem with the project was that almost whole logic was placed in about 10 functions and most of the functions exceeded 1000 LoC. Each code update involved plenty of headache with dynamic memory freeing. We spent about 1-2 days to compile it with C++ and replace the most crucial pointers with smart pointers, that we didn't have to care about memory freeing. That time we had to make the job done ASAP, surely we should have spend time in proper code refactoring.
Surely you need to modify C code to compile it with C++ and in the article we made an example of the modification. But the point is that the changes are pretty straightforward and small.
Also if you don't like my example with quick fix of some dirty project, then consider InnoDB storage engine which was developed in pure C for 2 decades, but now it's C++.
> If your C code happens to be compileable with a C++ compiler, then either your program is very small or you are writing really shitty C.
It is easy to write C code that is compatible with C++ if you have compatibility in mind from the beginning. Most of time: 1) cast malloc() – you can define macros for less code; 2) avoid C++ keywords; 3) don't use VLA which is not recommended anyway; 4) add __STDC_LIMIT_MACROS for uint64_t etc. Use -Wc++-compat also helps. It is harder to modify existing C code for the compatibility with C++.
> If your C code happens to be compileable with a C++ compiler, then either your program is very small or you are writing really shitty C.
And yet the standard practice for games written in C attempting to use Valve's C++ API for Steamworks integration is to switch to using a C++ compiler.
> It's also worth mentioning that the memory garbage collection (GC) in Java leads to high tail latencies and it's hard or even impossible to do anything with the problem
This is amusing, because a few days ago, there was an article¹ reporting that the C4 garbage collector solved this problem.
Solutions to pervasive, debilitating GC problems are as perennial as the Spring rain. Every year, we get a new crop, but the problems are still there the year after.
I've written my share of systems level C++ in performance constrained environments professionally (gamedev, embedded systems).
Never touched goto, alloca and branching usually stayed out of hot code through DOD approaches. Not really sure I agree with the conclusion.
Alternatively they didn't talk at all about aliasing/restrict which is one really neat area of Rust because &mut guarantees one of the key aliasing constraints.
I've used goto a handful of times over my 20 year professional history with c and c++. A few were used for simplifying cleanup code (actually this is quite common in c, less so in c++); a few were used in weird state machines and were accompanied by a page of documentation.
In principle, Rust could get faster if optimizers took advantage of latitude enabled by stronger semantic guarantees.
In practice, turning on such optimizations introduces debilitating bugs, because the optimizations are poorly exercised by other languages, and Rust core developers are fully occupied with other problems.
O(n) is the starting point of optimization. Improvement by factors of two or ten are commonplace, relying on details of how modern machines are implemented, and often lately by farming work out to multiple cores or to GPU cores.
Speed of the code is getting increasingly important as memory and storage sizes grow while clock speed stubbornly does not. Until Dennard scaling died, code got faster just by waiting. Now, making code faster takes work.
>The Rust's generics and macros are much weaker than provided by C++ templates coupled with C macros. Although, this is also not so crucial.
That's a very strange way to put it IMO, I'm not sure what is meant by "weaker" here. They are stricter and as such can require more work to use effectively, but weaker is a weird way to put it.
C++ template provides features that rust generics don't. I am not a Rust developer so correct me of some of these have been recently added. Rust generics being stricter would be something like them allowing to use only methods provided by trait, but I would call all of following design limitations making rust generics weaker.
* passing integer as template argument - from what I understand some of rust standard library functions can't handle arrays with more than 64 or 32 elements due to this or something similar. And there is active work for adding const generics to rust.
* variadic templates - there is a RFC for this
* passing template as template argument
* template specialization - C++ is somewhat moving away from this
The first point has had its restriction lifted as of the current version of Rust, 1.47.0. Or rather, the standard library and arrays bit is fixed; the underlying feature is not in stable Rust, and so other libraries still can't make this work properly yet.
Variadrics are desired by some folks, but aren't a particularly high priority.
Template template parameters are unlikely to land directly in Rust; GATs will be roughly equivalent. And specialization has a nightly implementation, and is highly desired, but isn't done yet.
If they meant "A more powerful tool", the right way to express that is "more powerful", not "weaker/stronger".
However, if you look at that part of the article, they justify that statement by linking to this post [0] which is explicitly about how rust actually type-checks generics more strongly. The linked post has the opposite implication of what the author took it to mean, and that link makes it clear that they were referring explicitly to how they're typed, not to "actual effectiveness".
> C++ templates are „untyped“ or very „weakly“ typed at the kindest, while Rust generics are „strongly“ typed.
Can you provide your definition of strongly typed?
According to my understanding, C++ templates are evaluated at compile time, and the resulting instance has to qualify all the static type checks of standard C++. The deduced types is always checked, and if there is no substitution fullfill the type check is found, it raises a compile error.
The errors are super ugly, as there are no type constraints for template parameters, but funnily enough the template errors in C++ reminded me of playing around with Haskell without using function declarations. I would find it odd to call Haskell though untyped or weakly typed because of that.
It should be said that C++ templates are dynamically typed (not weakly) and Rust generics are statically typed. But otherwise it is correct that Rust generics give a stronger guarantee than C++ templates.
It is beneficial to view C++ as a two-stage programming language, where the first stage runs templates and emits instances and the second stage runs them in the actual target. When C++ templates are said to be "untyped", that's really about the first stage [1]. You can't be very sure that templates act as intended for all valid inputs because it can't statically determine types before the first stage. Of course you can still write almost-working templates.
[1] Specifically the template invocation. C++ templates do have types for compile-time expressions and for that reason it was a wise move to move most of them into constexpr functions.
then again, the strongly and weakly typed monikers have no defined meaning and people will argue endlessly about definitions to the point that have very little value.
edit: the static/dynamic distinction pointed out elsethread is a much better description.
This is part of why I think we should begin to move away from “weaker” and “stronger” as adjectives for programming; these terms add a value judgement that is not properly reflective of the more nuanced trade offs involved.
Just occurred to me, since Rust has LLVM IR asm directives, can't you write a portable goto in an inline function that jumps directly to pointers? Publish that crate to crates.io? Problem solved.
Rust stable still does not have inline assembly. Additionally, we have moved away from the "whatever LLVM does" feature, and built our own.
There are significant language issues with doing something like this. Yes, in theory, you could do something. But it's going to be very unsafe, and very error prone.
I do see that the LLVM IR thing isn't being used anymore. I hope asm! ends up in stable Rust soon, seems like a pretty big oversight. Then again, MSVC dropped inline ASM altogether, which seems really dumb.
One of these days I'm going to write a crate that easily enables doing all the unsafe, dangerous, stupid things that C programmers want to do, including gotos, with no safety checks or variable lifespan verification of any kind. And I'm going to name it "evil". :^)
Just write your parser as a collection of functions that can use tail calls. You should end up with a similar structure, i.e. a block of code with jumps instead of calls.
Even in C++ I'd prefer that over a goto-based system (unless generated).
Especially because a "professional" programmer should be able to get along just fine without goto. Goto is bad practice in many languages where it does exist, too, like Go.
How do you break out of nested loops without an extra variable set/check to break on in the outer one otherwise?
goto is nasty, but in my experience writing HPC code, it does have some uses that can't be solved as efficiently (i.e. without extra branching) otherwise in some situations.
Cool - in which case, at least based on my experience, that would negate the need for goto in the main situation I end up using them in C/C++.
The only other time I've seen it be used fairly regularly is cleanup / return handling, where there are other ways of doing that (RAII structures, if you don't mind creating them), but sometimes using goto for that is just simpler / more obvious than having extra structs/classes to handle that.
I also suspect that goto is missing not because of potential misuse, but because it's actually difficult to implement given all of Rust's other semantics.
The only times I've really wanted goto were around exception handling. Java (not sure if it started there or was borrowed) actually has a construct that can do this without the issues with goto:
block: {
...
if (err){ break block; }
...
if (err){ break block; }
....
}
Rust has this innovative feature called "functions" which enables you to break from any block by just giving it a name and using "return". It can even return a value!
(/s obviously, but also not /s obviously. The serious point here may be that Java or whatever got in the way of defining functions by requiring them to be "methods" stuck in a "class" rather than, you know, just functions.)
A static function in Java is essentially just a function. Also I have no idea what functions being associated with classes has to do with breaking out of loops.
encapsulate your nested loops in a function and return to exit multiple loops at the same time. Having nested lightweight functions allows minimal syntactical overhead compared to plain loops, although still not zero.
I didn't mean anything condescending here actually. The point is that when people starting programming, it's good to put them into a restricted environment, so that they get used to use the right tools and in the right way. For example, it's quite easy to misuse goto in normal programs and make the programs unreadable. However, if a developer is already perfect with writing good code, but (s)he needs to do something special, like the state machine in the article, the restriction just makes the developer's life harder.
I have a question about those state machine: did you tried to do it without goto with functions only? The idea is a straightforward one, just chuck all branches into functions and call functions instead of goto. Rust wouldn't be happy with that, if you try to make branches into local functions, because they would access "global" mutable variables on stack. Borrow checker might misunderstand your intentions and go mad. But we could think a way around it, how about state as a struct-like variable passed by a mutable reference into inline functions?
Or maybe not functions, one might consider duplicating code. There is DRY principle, but there is WET principle[1] also. Both of them have their proponents, so at least they are not stupid. How WET shows itself in a highly-optimized programs? Did you try it? I have no link, and may be wrong, but I believe that John Carmack argued in a favor of WET, and I wouldn't say that Carmack doesn't bother with a program speed.
I mean, I'm not convinced, that goto is the game changer, I see other ways around the issue, which in some ways are superior to the solution using goto. So I'm not so sure that your way to use goto is not a misuse.
> if a developer is already perfect with writing good code, but (s)he needs to do something special, like the state machine in the article, the restriction just makes the developer's life harder.
The trouble is there are no developers who a perfect. So it is not intrinsically bad thing to make live of a developer harder by introducing restrictions. If it forces developer to codify more of his assumptions about his program, then it is harder to be imperfect.
The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all. We wanted one big function with is executed as linearly as possible to improve instruction cache prefetching.
> The talk also discusses code repetitions: compiler makes duplicate code anyway, even for straightforward `for` loop and nested `switch`, but in our case we had much more duplicate code.
I'm unable to find measurements. I mean, the amount of code duplication is not an optimization parameter, the amount of Mb/s of HTTP parsed is. It is obvious that the size of code leads to a greater cache use and slows down a program, but any engineer's decision should be an educated choice between alternatives with different upsides and downsides. Spagetti code with goto jumping between branches is a bad thing from the point of view of maintainability. From other hand constantly overflowing instruction caches also is a bad thing. Which one is worse? How to compare apples and oranges? I do not know how to quantify maintainability (pity), but throughput of a program could be quantified easily.
> The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all.
Function in C/C++ is a high-level abstraction, would it lead to a CPU executing 'call' instruction or not -- is a choice of a compiler and a programmer.
If we return to a premise of your article, that rust isn't good for a systems programming, I might notice, that many wouldn't agree with you that what you do is a systems programming worth mentioning. It is nice to know how much C++ code could be tuned for a maximum speed, but I'm not sure that I'd prefer hand crafted state machine to a parser generator, even if the latter would be much slower. If I choose hand-crafted state machine, I'd probably do it by splitting code as much as possible into a 'static inline' functions (most of them would be called just once), with meaningful names and some common arguments to make it as easy to understand and to reason about as possible. Yes, I'd sacrifice some of speed by this way, but I need not the fastest program possible, I need program that is fast enough and have other properties as well. There are more then one optimization parameter: it is systems programming.
Of course, your way have it's rights to exist, and probably it has it's niche too, but as I see it, it is a small narrow niche were available computational resources are scarce while throughput needed is high. In the most cases system software needs to be highly reliable, which needs code enabling us to reason about it, audit it, change it. It needs code which could be maintained by other people, not only those who wrote it.
> goto jumping between branches is a bad thing from the point of view of maintainability.
In our parser we have DSL for the state machine, so we encode which state the machine should go. The HTTP parser is one of the most updated piece of a web accelerator code and we don't struggle on the FSM - every developer in our team updates the code, even newcomers.
> Thus, in this single case when a Rust implementation is more or less faster than C, the performance difference is not about better compiler, but about more efficient structure of the program, which allows a compiler to optimize code better.
This is strange takeaway.... I would say that's almost the only thing that matters. They compared one Rust compiler to 3 C++ compilers and picked the best result? Who does that in practice - who compiles their codebase with 3 different compilers and picks the most efficient one for each object file? Also, the compiler can often improve, where as the language itself is much more difficult to improve - the fact that the language lets you to write a more efficient (and more readable!) structure is crucial.
Also, they seem to misunderstand the point of Rust entirely. The main point of Rust is "safety" (w/o sacrificing performance, yes - but safety was the primary design goal). And for a good reason! Systems programming is more about safe systems than it is about fast systems - fast buggy system programs are useless. The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
> The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
The "I know how to use goto [replace with any feature] everyone else is just stupid" reasoning upsets me so much.
While I addressed safety in a separate section in the article, I wouldn't argue about that: it seems Rust designers made the perfect work in safety. However, C++ is moving in this directly, bu there is the "gap" as it was described in the cited talk from the CppCon 2020.
However, there article is about FAST programming languages. Which means, and I stated this explicitly at the beginning of the article, that the main factor for the article is the speed of the generated code.
This is why I compared the single Rust implementation with 3 C/C++ implementations. The question was: whether Rust does something unreachable for C/C++? And the answer is "NOT". Also please keep in mind that all the benchmark programs, using the same algorithms, are still coded in bit different ways. And the differences impact performance significantly. I analyzed two programs, in Rust and C, to show the differences.
What is a "FAST" programming language? It can't be "it's theoretically possible to write the absolute fastest code using this" because then the answer to everything must be assembly.
Look e.g. at Rich Hickey's interviews - when building actual systems, the "theoretically fastest implementation" doesn't even matter that much in practice[1] - actual practical C++ code (written by experts!) can be slower than even Lisp code. I've witnessed this first hand, too; you've witnessed it too - in a micro-benchmark, no less - where Rust surprised you by being significantly faster than 3 state-of-the-art C++ compilers. But instead of concluding that Rust (the language) maybe did something terribly good, you conclude "meh, it's not that the compiler is better, it just got lucky in this case".
[1] I know there are a few domains where this matters (e.g. embedded systems). I'm not saying that "Lisp is better than C++" - I'm saying that your conclusions about Rust are... surprising, at least to me.
--gc:arc. Plain reference counting with move semantic optimizations, offers a shared heap. It offers deterministic performance for hard realtime systems. Reference cycles cause memory leaks, beware.
--gc:orc. Same as -gc:arc but adds a cycle collector based on "trial deletion". Unfortunately that makes its performance profile hard to reason about so it is less useful for hard realtime systems.
--gc:none. No memory management strategy nor garbage collector. Allocated memory is simply never freed. You should use --gc:arc instead.
Nim offers alloc, dealloc, allocShared, deallocShared
So in other words Nim does not actually offer a true compile-time memory management feature like Rust does? I'm afraid my impression of a language only decreases when I see its supporters dodge an issue like this...
Yes, if you want to operate in "unsafe" mode you can manage memory yourself with pointers and malloc/free. But it is rarely needed in practice. Also, Nim compiles to C or C++ (and also to Javascript). Because of that Nim's C/C++ FFI is natural ans wrapper-free.
> FreeBSD has been supporting C++ modules for a while.
No, we don't. The link provided is to a user forum where someone asks about it, and the first response is "I don't believe that this is a good idea." I don't know why the author would just make up this false statement, but IMO it really hurts their credibility.
The first response starts "I don't believe that this is a good idea", then makes some suggestions as to how to make it work anyway. There is plenty of room, at this point in consideration, for the thread to have reached a conclusion very different than that implied by the first sentence of the first comment.
Sure. I also skimmed through more of the thread than the first comment; it just made a nice sound-bite.
I happen to know TFA's sentence is bunk from first-hand experience with the FreeBSD kernel and C++ proposals, but wanted to show that even the linked forum post doesn't really support the author's claim.
But did you? My FreeBSD knowledge is quite outdated, I believe the last version I worked with is 7, but I believe at some point, it was possible to compile C++ kernel modules for FreeBSD. This time I can not find the proof, but plus to that post I also found links like
The last one mentions: "There's actually a fair amount of experience with people doing C++ in FreeBSD kernels. People have been doing things with it for about 8 years now."
People have always played around with C++ kernel modules outside the project (e.g., https://github.com/adamlsd/libcpp.ko ), but that is very different from saying the project supports C++ modules.
There are no C++ kernel modules in FreeBSD itself. There is no support for C++ kernel modules in FreeBSD itself (neither build system nor source code). Developer response to C++ kernel proposals (the most recent on private internal mailing lists I cannot share) is cold.
> A real high-level system programming language must be compatible with C.
I cannot agree with this. After I learned rust and tried to mix it with C code I believe that Stroustrup made a mistake by making C++ compatible with C on a syntax level. Strictly defined FFI boundary is a good thing. It liberates. It allow to track precisely what boundary APIs are, it makes boundary APIs the separate documented thing.
> An opposite example to use Rust for an Nginx module is CloudFlare's Quiche, an Nginx extension to support QUIC and HTTP/3 protocols. While it's definitely possible to use Rust for such kind of tasks, the guys, besides the FFI code for C/C++ bindings, still had to write some C code to patch Nginx.
I took a look at the patch, and it is not clear to me, that the patch was needed because rust wouldn't allow to do it the other way. It seems that cloudflare split their code into rust and C for some other reason, because there is a LOT of C code, it doesn't seem to me as a glue code at all. I believe it does more than just glue rust module and nginx. I'm not sure what the reason, but I could guess that the idea was to add possibility to use different implementations of QUIC and HTTP/3 in nginx, not just this particular rust implementation. So they extended nginx for this, and then implemented rust module.
I think Zig will make a great C replacement eventually. I pick it over C any day of the week. But I'd prefer Rust over either of them. Zig's management of heap-allocated memory is very similar to C, which makes it less than ideal for large projects.
Performance requirements have a huuuuuuuuuge range and Go can't really be considered for extreme cases. Like the latest, hottest, prettiest video gamez in VR HDR 8K resolution
"resource constrained" is a very subjective interpretation. OP is talking about SIMD intrinsics and compiler micro optimizations. Go is definitely out.
Rust would be compelling even if it didn't offer comparable performance to C. Its safety guarantees mean that much easier for "mediocre" programmers to feel like they can contribute meaningfully to large codebases without introducing massive memory leaks/unsound behavior/...
Exception epilogues also make optimization harder/impossible for compilers if I remember correctly. Rust suffers from this too though because panics use the same mechanism.
This is an exceptionally great article, which indeed discusses strong Rust limitations.
I have one question: the parser in the article uses computed goto, which, as far as I know, is not standard C. Are there many cases where even the standard goto yields significantly better performance?
Optimized tail calls are effectively the same as computed goto, but are a Standard feature. Or, rather, don't require non-standard syntax. A program that relies on the optimization built by a compiler that failed to implement it--which is allowed--would not be expected to run well.
We also found hat Ragel parser generator (using the goto state machine) generated somewhat faster large state machines than Bison (switch-drived state machine). However, as pointed out in the SCALE talk goto works better only on big enough state machines.
The article brings up some good points, but there are several misconception.
You can't compare normal user program execution with operating system kernel code execution. Total different beasts.
There are reasons, that most operating system kernels are written in C and assembly. And also there is a reason why kernel code seldom use any FPU or SIMD instructions beside saving and restoring the FPU and SIMD context on context switch for user threads. ( The size of the SIMD registers in byte x86_64 SSE (256) x86_64 AVX (512) x86_64 AVX-512 (2048))
Ex. You don't want your interrupt server code use any FPU/SIMD instructions, as you don't want to save and restore the register file for the FPU/SIMD registers for the kernel code. It's just takes to much time. And there are other architectural execution penalty on some CPUs when using large SIMD instruction codes.
We measured simple operating system functions, like page clear with SIMD, and it's just does not worth it even if we only use SIMD instructions in that function and save/restore only the necessary SIMD registers.
Also heavy inter operation between low level assembly and higher level system code ( C ) are essential in kernel. You have to be able to handle the same structures in assembly and C without any misalignment and misaddressing. ( we use special macros to write down kernel structures, that are used in both assembly and C, all of the assembly files are C preprocessed)
And there are other several issues with OS kernel codes (handling special registers, changing address spaces (virtual), invalidating/flushing caches (specially on ARM), managing any kind of exception (hard and soft, page fault, invalid instructions etc. ), real-time handling, kernel context handling if any, etc.).
I still don't see any misconceptions. The reason why the kernel uses SIMD with FPU save/restore is to optimize context switches. We addressed the topic in https://netdevconf.info/0x12/session.html?kernel-tls-handsha... . I guess early versions of WireGuard used the same approach: save FPU context at the beginning of softirq, process may packets with SIMD in one shot, and restore FPU state.
There are also other issues with the kernel code and I addressed them in the article, why it doesn't makes sense (while still possible) to use C++ in the kernel code.
Almost everything the article says about the prospects for using C++ in an OS kernel is wrong.
It is hard to understand how someone could know so many basic facts about the language and still be so thoroughly confused.
None of the reasons cited for not using C++ in kernel code is valid. None of the things claimed to be impossible are.
Example: RTTI. Really, RTTI is hardly ever used in good code. (I used in once in 10 years.) Despite its low use-value, nothing interferes with using it in a kernel.
Name mangling is absolutely no problem; neither would using `extern "C"`, if you wanted to.
Operator new is wholly compatible with kernel allocators.
The Standard Library does have read-write locks, although it is usually foolish to use such a lock in real code. It is trivial to wrap any synchronization primitive you like with a zero-overhead abstraction. Kernels tend to have their own anyway.
Exceptions in kernel threads would work identically the same as in user-level threads.
Static ctor sections could be run by kernel startup code as easily as they are run in regular programs. (Probably one would not bother running static dtor sections.)
I could go on and on, but enough.
This article joins others packed to the brim with out-and-out falsehoods. When you need to rally so many falsehoods to make your case, you end up making the opposite case--except where your audience is easily fooled.
Please read the discussion in https://www.reddit.com/r/Cplusplus/comments/jjtn5v/fast_prog... . In short, everything is doable, but before starting your project, which you're paid for, you have to grow quite a large C++ infrastructure, which double the native Linux kernel API in many ways.
> Operator new is wholly compatible with kernel allocators.
Please read the referenced thread. The things aren't so simple.
The Linux kernel is a large system. Things needed to work in Linux are not unusual for customizations needed for any large system, that we do routinely. If you think this is a problem, it can only be from lack of experience of large C++ systems.
When people complain about the complexity of C++, it is generally because they have no experience of large systems that demand all kinds of specializations. C++ can be used in a simple way to write simple programs, and then you get to ignore all the knobs and switches. It can also be used in all varieties of specialized environments, and then you find knob and switch settings to make it work for that environment.
155 comments
[ 6.1 ms ] story [ 261 ms ] threadI have been trying to find a good way to deal with hybrid memory/disk structures. One example is external sort, but there are a whole class of such structures used for databases (Hash Join, Hash Aggregation, etc.). The idea is to constrain the memory footprint of a structure and efficiently use the disk.
One challenge is simlly knowing how much memory is being used. Another is supporting multiple heaps/arenas/whatever and allocating in the right one. And a third is safely clearing parts of the structure that have been written to disk for later processing (I guess calling the destructors if necessary?).
Is there any work in this area?
If you make a data structure that is meant to use one contiguous chunk of memory and access memory linearly and/or with locality when possible, I would think it would make explicit disk/hybrid scenarios much less necessary.
Compare that to a Grace HashJoin (https://en.m.wikipedia.org/wiki/Hash_join) which partitions both sides (lookup side and hash table side) into matching partition pairs. The partitioning step and reading the partitions from disk are both sequential access. Then, pick up and process one pair of partitions at a time, which will only require a small hash table in memory.
Note that this is still way more efficient even when on an SSD, because an SSD is still block-oriented, and paging entire blocks in/out for a single lookup is still inefficient.
It even helps when the data fits into memory sometimes, because you can size the partitions small enough to fit in cache, which will make lookups even faster.
You can say "don't use hash tables", but that's not a great amswer for a low-level high-performance language like Rust.
In this case it seems like the downsides you mentioned are the downsides of keeping things on disk, not memory mapping (which would only hit the disk on the first load and would automatically use any available memory). Organization to use disk pages effectively would benefit memory mapping as well.
Memory mapping serves a similar purpose as temp files: a way to tell the OS that it can freely page that out to avoid uncontrolled swapping. But you need to do the organization yourself to keep that access sequential, which is hard to do in rust for the reasons that I mentioned.
If you memory map a hash table and have memory pressure, you'll go into the crazy bad scenario of a page-in for every lookup, which is avoidable with better organization.
I think we are basically in agreement but perhaps my point was not clear.
When I talk about organizing the data structure I'm talking about how the data is layed out in the memory map and how locality is affected.
What I'm saying is not that I think all uses are covered by memory mapping, just that data structure layout combined with memory mapping can probably take care of lots of the majority of scenarios.
I did not know that organizing memory layout was difficult in Rust since it is very straight forward in C++.
(1) knowing how much memory a particular structure (like a hash table) is using
(2) managing that memory as a single chunk; e.g. to free the whole thing at once so that you can pick up another chunk that you had previously serialized
It's not impossible in rust, it's just that you have to build up all the pieces and none of it will integrate nicely with the rest of the rust code.
Box and Vec are starting to gain the ability to be parameterized over an allocator.
> Is there any work in this area?
Kind of, but it's slow going. You can support multiple heaps/arenas/whatever, but it means you have to implement your own. As the ecosystem standardizes around the allocator traits (only the global one is stable yet), there will be easier interop.
I believe they're talking about custom allocator support for Box [0, 1]. The author of the PR says that they intend to add per-container allocator support once support for Box lands, starting with Vec [2].
[0]: https://www.reddit.com/r/rust/comments/jgxgpu/box_will_have_...
[1]: https://github.com/rust-lang/rust/pull/77187
[2]: https://www.reddit.com/r/rust/comments/jgxgpu/box_will_have_...
What? Rust allocates on the stack by default.
`void foo(int n) { int values[n]; }`
I don't know that that's a bad thing. AFAIK the implementation details are underspecified even in C and compilers kind of do whatever they want (i.e. I've seen some embedded compilers that monomorphize it in multiples of 2 up to a limit and just hope the user doesn't exhaust the stack).
Edit: I'm pretty sure you can create a hacky version using iterators and a recursive function: `fn vla<T>(iter: impl Iter<Item=T>, init: T, index: usize, count: usize) { ... }`
Edit2: Actually he's talking about `alloca` which is a whole other "here be dragons" feature. See the reddit thread steveklabnik linked for some of the pitfalls
BUT:
- it's not implemented (maybe partially? idk.)
- it has unclear/non-trivial interactions with async-await transformation, ~mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case~ Edit: It turns out it's actually not really possible to use unsized values in generator which life across await/yield boundaries, as far as I can tell.
So it's one of the "it's not hear yet" situations.
> - it has unclear/non-trivial interactions with async-await transformation, mainly the generator does need support for multiple unsized values, which isn't supported by rust but can be archived through some hacks in that specific case.
This especially gives me pause. I've thoroughly enjoyed the async/await experience in Rust despite its rough edges but it seems the community's exploration of async/await is still in its early stages. It sounds that something as complex unsized rvalues will take a long while to get right (I don't know what I'm talking about though, so please correct me!)
The problem is that when you create a generator it doesn't yet know the unsized field it only knows it when creating it in a `poll` call. But it only will call poll after pinning the generator at which point you can't really grow the size of it for a bunch of reasons including that growing might move it in memory (realloc) which you can't due to self references...
So you would need the size of the generator when generating it.
Which could somewhat be possible but not really with current rust features, i.e. it would need a lot a special care.
For me the downsides outweigh the upsides of alloca and I usually find other approaches(free list, arenas, etc).
AFAIK, in practice, many C++ compilers support alloca for compatibility with gcc (or more specifically, its C library)
Also, is alloca part of any C standard? I can’t find evidence for that easily. Also, if so, https://en.wikipedia.org/wiki/Stack-based_memory_allocation#... needs updating. It currently says
“The function was present on Unix systems as early as 32/V (1978), but is not part of Standard C or any POSIX standard.”
I also don’t see how it could be part of the standard, as that, AFAIK, doesn’t even specify the existence of a call stack (a compiler is free to allocate call frames on the heap, for example)
In the same way I could totally define alloca in a C symbol and then just extern it into Rust if I wanted to.
[edit]
Realized I was thinking of VLAs but I think the point still stands.
VLAs are different, I think.
AFAIK VLA is already that, didn't Linux de-VLA the kernel a few years back because they were a common security issue and the code they generate is bad / slow?
If your C code happens to be compileable with a C++ compiler, then either your program is very small or you are writing really shitty C.
Simplest example is that a `void func()` decl in C and C++ have different semantic meanings.
You can also tell that the C++ standard committee agrees with this to some extent since they tend to add C novelties into the C++ standards whenever possible to make it easier to write compatible code, for instance: https://en.wikipedia.org/wiki/C%2B%2B11#Improved_C_compatibi...
What's happened is the direct opposite of C++ where every fad gets added to the language. With C lots of standard proven features don't make it into the language.
It's a bit like saying that operating the windscreen wipers is a subset of knowing how to drive a car. It's strictly correct, but it's probably not where you should start nor where you should put most of your attention if you're looking to get you license.
The Lua interpreter is written in a subset of C so that it also compiles fine as C++. Or, if you prefer, in a subset of C++ so that it compiles fine as C.
https://www.lua.org/pil/24.1.html
Requiring explicit casts, and forbidding implicit conversions, reduces the odds of being surprised by an unwanted conversion. There's a good argument to be made for adding explicit casts to C code even when the language doesn't require them. GCC's -Wconversion flag can help with this.
There's also no ambiguity. You've already explicitly defined the variable's type at that point.
There is harm in implicit conversions catching the programmer unawares. This is a fairly significant cause of bugs in C code, and it's why C++ uses stricter rules, requiring you to always make these conversions explicit, even when using malloc.
I would also argue that there's a hard to quantify harm in making code more verbose. More visual noise makes it harder to parse what's going on. Sure, one redundant cast isn't going to kill you. But many sources of repetition across an entire code base eventually turn it into a slog where the parts you want are obscured by useless noise.
Yes.
> That isn't a significant burden.
But once you do it your "C" code is littered with a lot of unnecessary casts that increase the verbosity of the code and could hide errors. With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
How? As I said, the stricter rules of C++ result in fewer type-conversion errors than C's permissive rules. That's the reason for C++'s stricter rules, and it's the reason Ada is even stricter.
As an example, consider
In a languages with permissive rules about implicit conversions, you have to inspect the variables' types to determine whether this statement is equivalent to (Even ignoring OOP, that is.)> With pre C99 code it would also hide default int errors if the include for malloc gets lost at some point.
Wouldn't it just fail to compile if you're missing a required header?
edit steveklabnik linked to [0] so I see what you mean now. I think that's a valid point.
[0] https://stackoverflow.com/a/605858/
Even then, I never use the latest standards of C++, I stay a couple standards behind so I can rely on whatever compiler I pick being able to handle it. Right now I'm using C++14.
Surely you need to modify C code to compile it with C++ and in the article we made an example of the modification. But the point is that the changes are pretty straightforward and small.
It is easy to write C code that is compatible with C++ if you have compatibility in mind from the beginning. Most of time: 1) cast malloc() – you can define macros for less code; 2) avoid C++ keywords; 3) don't use VLA which is not recommended anyway; 4) add __STDC_LIMIT_MACROS for uint64_t etc. Use -Wc++-compat also helps. It is harder to modify existing C code for the compatibility with C++.
And yet the standard practice for games written in C attempting to use Valve's C++ API for Steamworks integration is to switch to using a C++ compiler.
[1] https://github.com/nospaceships/raw-socket-sniffer/blob/mast...
[2] https://github.com/GirkovArpa/raw-socket-sniffer/blob/master...
This is amusing, because a few days ago, there was an article¹ reporting that the C4 garbage collector solved this problem.
¹=https://news.ycombinator.com/item?id=24895395
Solutions to pervasive, debilitating GC problems are as perennial as the Spring rain. Every year, we get a new crop, but the problems are still there the year after.
Never touched goto, alloca and branching usually stayed out of hot code through DOD approaches. Not really sure I agree with the conclusion.
Alternatively they didn't talk at all about aliasing/restrict which is one really neat area of Rust because &mut guarantees one of the key aliasing constraints.
https://github.com/rust-lang/rust/issues/54878
In practice, turning on such optimizations introduces debilitating bugs, because the optimizations are poorly exercised by other languages, and Rust core developers are fully occupied with other problems.
O(n) is the starting point of optimization. Improvement by factors of two or ten are commonplace, relying on details of how modern machines are implemented, and often lately by farming work out to multiple cores or to GPU cores.
Speed of the code is getting increasingly important as memory and storage sizes grow while clock speed stubbornly does not. Until Dennard scaling died, code got faster just by waiting. Now, making code faster takes work.
That's a very strange way to put it IMO, I'm not sure what is meant by "weaker" here. They are stricter and as such can require more work to use effectively, but weaker is a weird way to put it.
Words are hard.
* passing integer as template argument - from what I understand some of rust standard library functions can't handle arrays with more than 64 or 32 elements due to this or something similar. And there is active work for adding const generics to rust.
* variadic templates - there is a RFC for this
* passing template as template argument
* template specialization - C++ is somewhat moving away from this
Variadrics are desired by some folks, but aren't a particularly high priority.
Template template parameters are unlikely to land directly in Rust; GATs will be roughly equivalent. And specialization has a nightly implementation, and is highly desired, but isn't done yet.
That’s the main difference in these languages features.
Saying that Rust generics are „weaker“ than C++ templates is technically incorrect, since they are actuall „stronger“.
Using these terms to mean something else is definitely „weird“.
Like you're describing the type system, they may be talking about actual effectiveness.
Like productivity diff between python and others
Basically, I'm not smart enuf to use rust
However, if you look at that part of the article, they justify that statement by linking to this post [0] which is explicitly about how rust actually type-checks generics more strongly. The linked post has the opposite implication of what the author took it to mean, and that link makes it clear that they were referring explicitly to how they're typed, not to "actual effectiveness".
[0]: https://users.rust-lang.org/t/generic-functions-c-vs-rust/21...
Can you provide your definition of strongly typed?
According to my understanding, C++ templates are evaluated at compile time, and the resulting instance has to qualify all the static type checks of standard C++. The deduced types is always checked, and if there is no substitution fullfill the type check is found, it raises a compile error.
The errors are super ugly, as there are no type constraints for template parameters, but funnily enough the template errors in C++ reminded me of playing around with Haskell without using function declarations. I would find it odd to call Haskell though untyped or weakly typed because of that.
It is beneficial to view C++ as a two-stage programming language, where the first stage runs templates and emits instances and the second stage runs them in the actual target. When C++ templates are said to be "untyped", that's really about the first stage [1]. You can't be very sure that templates act as intended for all valid inputs because it can't statically determine types before the first stage. Of course you can still write almost-working templates.
[1] Specifically the template invocation. C++ templates do have types for compile-time expressions and for that reason it was a wise move to move most of them into constexpr functions.
edit: the static/dynamic distinction pointed out elsethread is a much better description.
Uh oh... I love template specialization... what replaces this?
There are significant language issues with doing something like this. Yes, in theory, you could do something. But it's going to be very unsafe, and very error prone.
One of these days I'm going to write a crate that easily enables doing all the unsafe, dangerous, stupid things that C programmers want to do, including gotos, with no safety checks or variable lifespan verification of any kind. And I'm going to name it "evil". :^)
I will do it, eventually. ^^
Even in C++ I'd prefer that over a goto-based system (unless generated).
It's a bit condescending. I think the article would have been better without that kind of comment.
goto is nasty, but in my experience writing HPC code, it does have some uses that can't be solved as efficiently (i.e. without extra branching) otherwise in some situations.
The only other time I've seen it be used fairly regularly is cleanup / return handling, where there are other ways of doing that (RAII structures, if you don't mind creating them), but sometimes using goto for that is just simpler / more obvious than having extra structs/classes to handle that.
https://news.ycombinator.com/item?id=9827276
https://mail.mozilla.org/pipermail/rust-dev/2014-March/00914...
(/s obviously, but also not /s obviously. The serious point here may be that Java or whatever got in the way of defining functions by requiring them to be "methods" stuck in a "class" rather than, you know, just functions.)
Or maybe not functions, one might consider duplicating code. There is DRY principle, but there is WET principle[1] also. Both of them have their proponents, so at least they are not stupid. How WET shows itself in a highly-optimized programs? Did you try it? I have no link, and may be wrong, but I believe that John Carmack argued in a favor of WET, and I wouldn't say that Carmack doesn't bother with a program speed.
I mean, I'm not convinced, that goto is the game changer, I see other ways around the issue, which in some ways are superior to the solution using goto. So I'm not so sure that your way to use goto is not a misuse.
> if a developer is already perfect with writing good code, but (s)he needs to do something special, like the state machine in the article, the restriction just makes the developer's life harder.
The trouble is there are no developers who a perfect. So it is not intrinsically bad thing to make live of a developer harder by introducing restrictions. If it forces developer to codify more of his assumptions about his program, then it is harder to be imperfect.
[1] https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all. We wanted one big function with is executed as linearly as possible to improve instruction cache prefetching.
I'm unable to find measurements. I mean, the amount of code duplication is not an optimization parameter, the amount of Mb/s of HTTP parsed is. It is obvious that the size of code leads to a greater cache use and slows down a program, but any engineer's decision should be an educated choice between alternatives with different upsides and downsides. Spagetti code with goto jumping between branches is a bad thing from the point of view of maintainability. From other hand constantly overflowing instruction caches also is a bad thing. Which one is worse? How to compare apples and oranges? I do not know how to quantify maintainability (pity), but throughput of a program could be quantified easily.
> The thing is that we used hot/cold labels to minimize jumps over the code and improve instruction cache usage, so calling functions isn't good at all.
Function in C/C++ is a high-level abstraction, would it lead to a CPU executing 'call' instruction or not -- is a choice of a compiler and a programmer.
If we return to a premise of your article, that rust isn't good for a systems programming, I might notice, that many wouldn't agree with you that what you do is a systems programming worth mentioning. It is nice to know how much C++ code could be tuned for a maximum speed, but I'm not sure that I'd prefer hand crafted state machine to a parser generator, even if the latter would be much slower. If I choose hand-crafted state machine, I'd probably do it by splitting code as much as possible into a 'static inline' functions (most of them would be called just once), with meaningful names and some common arguments to make it as easy to understand and to reason about as possible. Yes, I'd sacrifice some of speed by this way, but I need not the fastest program possible, I need program that is fast enough and have other properties as well. There are more then one optimization parameter: it is systems programming.
Of course, your way have it's rights to exist, and probably it has it's niche too, but as I see it, it is a small narrow niche were available computational resources are scarce while throughput needed is high. In the most cases system software needs to be highly reliable, which needs code enabling us to reason about it, audit it, change it. It needs code which could be maintained by other people, not only those who wrote it.
The measurements are covered in slides 23-24 in http://www.tempesta-tech.com/research/http_str.pdf
> goto jumping between branches is a bad thing from the point of view of maintainability.
In our parser we have DSL for the state machine, so we encode which state the machine should go. The HTTP parser is one of the most updated piece of a web accelerator code and we don't struggle on the FSM - every developer in our team updates the code, even newcomers.
This is strange takeaway.... I would say that's almost the only thing that matters. They compared one Rust compiler to 3 C++ compilers and picked the best result? Who does that in practice - who compiles their codebase with 3 different compilers and picks the most efficient one for each object file? Also, the compiler can often improve, where as the language itself is much more difficult to improve - the fact that the language lets you to write a more efficient (and more readable!) structure is crucial.
Also, they seem to misunderstand the point of Rust entirely. The main point of Rust is "safety" (w/o sacrificing performance, yes - but safety was the primary design goal). And for a good reason! Systems programming is more about safe systems than it is about fast systems - fast buggy system programs are useless. The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
> The authors decry the loss of goto saying it's "good for juniors but too limiting for professionals" - as if professionals aren't humans too! I'm sorry to say that, but whenever I saw that attitude before - it was with programmers that greatly overestimated their skill level.
The "I know how to use goto [replace with any feature] everyone else is just stupid" reasoning upsets me so much.
However, there article is about FAST programming languages. Which means, and I stated this explicitly at the beginning of the article, that the main factor for the article is the speed of the generated code.
This is why I compared the single Rust implementation with 3 C/C++ implementations. The question was: whether Rust does something unreachable for C/C++? And the answer is "NOT". Also please keep in mind that all the benchmark programs, using the same algorithms, are still coded in bit different ways. And the differences impact performance significantly. I analyzed two programs, in Rust and C, to show the differences.
Look e.g. at Rich Hickey's interviews - when building actual systems, the "theoretically fastest implementation" doesn't even matter that much in practice[1] - actual practical C++ code (written by experts!) can be slower than even Lisp code. I've witnessed this first hand, too; you've witnessed it too - in a micro-benchmark, no less - where Rust surprised you by being significantly faster than 3 state-of-the-art C++ compilers. But instead of concluding that Rust (the language) maybe did something terribly good, you conclude "meh, it's not that the compiler is better, it just got lucky in this case".
[1] I know there are a few domains where this matters (e.g. embedded systems). I'm not saying that "Lisp is better than C++" - I'm saying that your conclusions about Rust are... surprising, at least to me.
[1] https://nim-lang.org/
Nim offers alloc, dealloc, allocShared, deallocShared
> FreeBSD has been supporting C++ modules for a while.
No, we don't. The link provided is to a user forum where someone asks about it, and the first response is "I don't believe that this is a good idea." I don't know why the author would just make up this false statement, but IMO it really hurts their credibility.
I read the whole thing. It doesn't.
I happen to know TFA's sentence is bunk from first-hand experience with the FreeBSD kernel and C++ proposals, but wanted to show that even the linked forum post doesn't really support the author's claim.
https://lists.freebsd.org/pipermail/freebsd-hackers/2009-Feb... https://lists.freebsd.org/pipermail/freebsd-hackers/2006-Jul...
The last one mentions: "There's actually a fair amount of experience with people doing C++ in FreeBSD kernels. People have been doing things with it for about 8 years now."
There are no C++ kernel modules in FreeBSD itself. There is no support for C++ kernel modules in FreeBSD itself (neither build system nor source code). Developer response to C++ kernel proposals (the most recent on private internal mailing lists I cannot share) is cold.
I cannot agree with this. After I learned rust and tried to mix it with C code I believe that Stroustrup made a mistake by making C++ compatible with C on a syntax level. Strictly defined FFI boundary is a good thing. It liberates. It allow to track precisely what boundary APIs are, it makes boundary APIs the separate documented thing.
> An opposite example to use Rust for an Nginx module is CloudFlare's Quiche, an Nginx extension to support QUIC and HTTP/3 protocols. While it's definitely possible to use Rust for such kind of tasks, the guys, besides the FFI code for C/C++ bindings, still had to write some C code to patch Nginx.
I took a look at the patch, and it is not clear to me, that the patch was needed because rust wouldn't allow to do it the other way. It seems that cloudflare split their code into rust and C for some other reason, because there is a LOT of C code, it doesn't seem to me as a glue code at all. I believe it does more than just glue rust module and nginx. I'm not sure what the reason, but I could guess that the idea was to add possibility to use different implementations of QUIC and HTTP/3 in nginx, not just this particular rust implementation. So they extended nginx for this, and then implemented rust module.
People repeat this like Go isn’t designed for use in resource constrained environments.
AVR support is more of a work in progress than the Cortex-M support, but it does exist.
I have one question: the parser in the article uses computed goto, which, as far as I know, is not standard C. Are there many cases where even the standard goto yields significantly better performance?
It is very unlikely that the computed gotos make the parser faster than using standard features of a more powerful language.
Can you explain more on how? Use case I have in mind is bytecode interpretation where computed goto should be a big help.
https://github.com/ncm/computed-goto
The "more powerful language" part is to get language-level tools to compose parser combinators, ultimately resolving down to tail calls.
Regarding computed and simple goto I'd like to reference our early article discussing the parser in standard goto https://natsys-lab.blogspot.com/2014/11/the-fast-finite-stat... . My recent talk at scale https://www.socallinuxexpo.org/scale/17x/presentations/fast-... (watch videohttps://www.youtube.com/watch?v=LQc4er8ng64&feature=youtu.be... and slides at http://www.tempesta-tech.com/research/http_str.pdf ) discusses the parser with the compiler extensions.
We also found hat Ragel parser generator (using the goto state machine) generated somewhat faster large state machines than Bison (switch-drived state machine). However, as pointed out in the SCALE talk goto works better only on big enough state machines.
Ex. You don't want your interrupt server code use any FPU/SIMD instructions, as you don't want to save and restore the register file for the FPU/SIMD registers for the kernel code. It's just takes to much time. And there are other architectural execution penalty on some CPUs when using large SIMD instruction codes.
We measured simple operating system functions, like page clear with SIMD, and it's just does not worth it even if we only use SIMD instructions in that function and save/restore only the necessary SIMD registers.
Also heavy inter operation between low level assembly and higher level system code ( C ) are essential in kernel. You have to be able to handle the same structures in assembly and C without any misalignment and misaddressing. ( we use special macros to write down kernel structures, that are used in both assembly and C, all of the assembly files are C preprocessed)
And there are other several issues with OS kernel codes (handling special registers, changing address spaces (virtual), invalidating/flushing caches (specially on ARM), managing any kind of exception (hard and soft, page fault, invalid instructions etc. ), real-time handling, kernel context handling if any, etc.).
There are also other issues with the kernel code and I addressed them in the article, why it doesn't makes sense (while still possible) to use C++ in the kernel code.
It is hard to understand how someone could know so many basic facts about the language and still be so thoroughly confused.
None of the reasons cited for not using C++ in kernel code is valid. None of the things claimed to be impossible are.
Example: RTTI. Really, RTTI is hardly ever used in good code. (I used in once in 10 years.) Despite its low use-value, nothing interferes with using it in a kernel.
Name mangling is absolutely no problem; neither would using `extern "C"`, if you wanted to.
Operator new is wholly compatible with kernel allocators.
The Standard Library does have read-write locks, although it is usually foolish to use such a lock in real code. It is trivial to wrap any synchronization primitive you like with a zero-overhead abstraction. Kernels tend to have their own anyway.
Exceptions in kernel threads would work identically the same as in user-level threads.
Static ctor sections could be run by kernel startup code as easily as they are run in regular programs. (Probably one would not bother running static dtor sections.)
I could go on and on, but enough.
This article joins others packed to the brim with out-and-out falsehoods. When you need to rally so many falsehoods to make your case, you end up making the opposite case--except where your audience is easily fooled.
> Operator new is wholly compatible with kernel allocators.
Please read the referenced thread. The things aren't so simple.
They should demand powerful, but too often fail to.
The Linux kernel is a large system. Things needed to work in Linux are not unusual for customizations needed for any large system, that we do routinely. If you think this is a problem, it can only be from lack of experience of large C++ systems.
When people complain about the complexity of C++, it is generally because they have no experience of large systems that demand all kinds of specializations. C++ can be used in a simple way to write simple programs, and then you get to ignore all the knobs and switches. It can also be used in all varieties of specialized environments, and then you find knob and switch settings to make it work for that environment.
I am learning rust but what criteria any language must meet to call itself mature?
News at 11.