Maximal min() and max()
lwn.net
lwn.net
The macros now do type-safe comparisons that work correctly with combinations of different argument types where this is possible, work in a constant context (eg defining array bounds), work correctly in the face of implicit type coercion and include a 3-way min and max that have these same desirable properties (same as the two way min and max).
The problem(s) are it’s a bit slow to compile and the preprocessor expansion (although not necessarily the final generated assembly - didn’t see anyone saying that) is a bit bloated.
So when you make a “why don’t they just…. ?”-suggestion, make sure your suggestion is at least as good as what they have now in terms of the desirable functionality and correctness and then tackles the actual problems in some way. I’m not sure all of the suggestions I have seen here and in the lwn comments succeed at meeting those two criteria.
The thing they want seems perfectly reasonable to me and wouldn’t only benefit the kernel.
If somebody proposed a new extension and got llvm and gcc to both implement it today, they would still need something to work on old compilers. The oldest GCC supported by Linux is 10 years old nearly.
If a new compiler extension was proposed and implemented and released in both llvm and gcc, some years after that they could drop support. But there would be no real imperative to drop support early since they will already have developed some code that works okay on those toolchains.
No, the point was for today's compilers they could have something that compiles quickly and does everything they want.
Elaborate? Why?
I am saying they could have either implemented this in the past (and thus had it land by now) or implement it now (and have it land in e.g. GCC in the near future). Neither appears to be the case. I was not suggesting they have a time machine. I can't understand what you're finding so difficult about this.
This really isn't rocket science. They need something that works in the kernel on today's compilers. The end.
I have no idea how you make this stuff up, but Linux and GCC have thousands of developers working on them from all across the world, and Linux heavily takes advantage of GCC built-ins. There even exist active maintainers who understand and contribute to both... not that that is in any shape or form a requirement for putting out a request from one project to the other. The idea that they would've been unable to find a single person with sufficient interest & understanding of compilers to implement min/max in it is downright absurd. If they wanted to do this someone would have implemented it a long time ago. And yes, the end.
The Linux kernel is one of the few cases where just the sheer scale of instances may make a difference
Of course there are others than C. But not many suitable for writing a kernel. And no obvious choice for what Linux could do today. Of course they are starting with Rust, but nobody can predict when that will make the last C macro unnecessary. Unless your prediction is never...
Even Linus accepting Rust is kind of interesting, because his C++ rants also apply to plenty of Rust code bases.
Whereas C++ tends to make you eat the whole enchilada.
Also see Embedded C++ as used by macOS IO Kit, which thankfully there isn't a Linus at Apple.
Regarding Rust, no_std, doesn't change the npm like dependencies, macro festival (with two ways of doing all kinds of stuff), abstraction party with type driven development, the compile times.
Partly this is because Rust thought about this earlier, and partly it's because of the relationship between C++ and the allocator, having operators for a feature which might not even exist on your target, awkward.
This doesn't mean using C++ for kernels and such is impossible, but anyone with a conservative, risk-averse mindset (like Linus) isn't going to want to touch it.
Rust, on the other hand, was deliberately designed for such uses, starting from a fresh sheet of paper. Giving systems programmers everything they want without any hidden "gotchas" is its whole raison d'être.
My strong impression is that both Apple and Microsoft are moving away from C++ in their core systems.
C++, otoh, is run by committee and far removed from the community. (C is not much better, but at least they are stable)
It is hard to be close to a community with the size and divergence of the C++ one. It is (relatively) easy to be close to a community the size of the Rust one.
If you would then say "why doesn't min/max just implement a switch on each primitive type", I think on some level it does just do that.
I think implicit type conversion is a mistake, perhaps one of the few true design flaws in C. Languages like haskell and rust went with explicit conversions which is probably a better idea overall, even if it does increase the code verbosity a bit. C++ instead doubled down and added many more ways for implicit conversions to happen.
but also allows you to turn them off for objects, thankfully.
(void) (&_x == &_y);
Is this... a check for type-compatibility? I don't think actually produces a compilation error if the types are incompatible.Which, I mean... it's the Internet. That's kinda expected. But if there's something specific you're seeing, could you link to it? I'm curious as well what the "stick to C" crowd's reasons are. "C plus this one feature of C++" seems rather defensible at a glance (ignoring the social difficulty in choosing which feature), but I'm sure it's much more complicated than that in practice.
There's ample evidence that it'll build just a fast as C there, so it's not an issue in this context, and that both can build quickly with care. That's kinda the point of C++: you can write plain C code plus [this one thing] and you basically don't pay for the rest, and it generally achieves that.
It’s all in the thread, I don’t know how GP missed it.
$ cd /usr/src/linux
$ rg ' class;'
vmlinux.h
8541:struct class;
13515: long unsigned int class;
16416: unsigned int class;
16917: u8 class;
17351: u8 class; cgci = XCreateGC(
display,
window,
GCForeground | GCBackground | GCFont,
&(XGCValues) {
.foreground = ccwhite,
.background = ccblack,
.font = cfont->fid,
}
);
[1] C++ requires the fields to be initialized in order; C doesn't have this restriction.[1] From a C++ is just a C superset perspective. In C compound literals are awesome.
It's not impossible, but the Linux kernel is quite a complex project.
You'll be happy to know that GCC happily compiles C++ then! No need to switch to MSVC.
I'm fact GCC itself successfully switched to C++ from C a few years ago.
I'm unfamiliar with the incantations needed to try preserve constant expressions though, that might be too much for them.
[1] `_Generic` also requires at least two because you need one copy to select the generic implementation and another to call it. C23 `typeof` (or the equivalent GNU extension) allows for a compact type tuple matching:
inline static int max_int(int x, int y) { return x > y ? x : y; }
inline static unsigned max_uint(unsigned x, unsigned y) { return x > y ? x : y; }
/* ... */
#define MAX(a, b) (_Generic( \
(void (*)(typeof(a), typeof(b))) NULL, \
void (*)(int, int): max_int, \
void (*)(unsigned, unsigned): max_uint, \
/* ... */ \
default: max_type_error \
) (a, b)) struct S {
int foo[max(10, 15)];
}
Isn’t possible in C, macros are the only option.So even if you were to try to use _Generic in a macro to handle type correct dispatch you would not be able to use that macro in many of the contexts it is needed.
Honestly constexpr is something that would really help C, and does not need to bring in any other c++ features. Although I guess in this case the lack of the full template and such features set would mean matching the kernel requirements would still require a macro+_Generic to adopt.
These macros are now so complex that they're reluctant to touch them. Seems like there is a clear need for some thorough tests here? This is exactly the sort of thing that is eminently testable.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char**argv){
puts("hello, world");
return EXIT_SUCCESS;
}
came to something ridiculous on the order of 100k lines.(tested with `gcc -E - | wc -l`)
However, hygienic macros that fix entire classes of issues (including this) is a more compelling argument.
Not to say that it makes the case, only that the strawman you wrote is not good.
Not necessarily.
The preprocessor code here picks up the original source, and blows up the initial code (which is about "min3(long_a, long_b, long_c)") to 47 MB of code. no fancy stuff, just 47MB of C-Code on the disk. That's a lot of code the compiler then has to parse and handle.
If hygienic macros are a first-class citizen in the compiler, the compiler parses the original macro code once and then just modifies it in-memory. There is no reason to write 47MB of code somewhere and read it back, this would just happen as an AST modification in memory.
But that is also a much smaller reason. First-class macros allow the compiler to reason based off of the types and structure of the macro inputs. You don't have to guess if something is constant, bounded, unbounded and such. Strong types can enforce this safely and macros and optimization can use these strong guarantees. And sufficiently strong type information can open doors for far, far more powerful optimizations overall.
Just for the record - I'm fully aware why the kernel is where it is, and why it will stay there, but there is far improved compiler and language theory from there.
Given the number of preprocessor hacks used in the kernel, and the amount of GCC-specific behavior that the codebase depends on, it seems like they are already halfway there.
Stop using tiny hammers and get a big one.