|
|
Subscribe / Log in / New account

Maximal min() and max()

By Jonathan Corbet
August 1, 2024
Like many projects written in C, the kernel makes extensive use of the C preprocessor; indeed, the kernel's use is rather more extensive than most. The preprocessor famously has a number of sharp edges associated with it. One might not normally think of increased compilation time as one of them, though. It turns out that some changes to a couple of conceptually simple preprocessor macros — min() and max() — led to some truly pathological, but hidden, behavior where those macros were used.

min() and max() for the kernel

Your editor's well-worn, first-edition copy of The C Programming Language introduces the preprocessor with this example:

    #define max(A, B) ((A) > (B) ? (A) : (B))

The hazards that come with a macro like this, such as the double evaluation of the arguments, were pointed out in the text. Still, that did not prevent kernel developers from making use of it; as covered here in 2001, there were over 150 definitions of min() and max() matching the above pattern in the 2.4.8 kernel.

At that time, Linus Torvalds decided that a consolidation made sense; he added a single set of those macros meant to be used throughout the kernel. He also changed the interface, though, adding a type parameter describing how the comparison is to be performed — signed or unsigned integer, for example. The goal was to increase correctness, but the immediate effect was to break compilation throughout the kernel; the result was a classic linux-kernel flame war of the type that, fortunately, tends not to happen anymore.

Despite the complaints, the changes stuck — briefly. When the 2.4.9.8 release came about in February 2002, it included a change described as: "make the three-argument (that everybody hates) 'min()' be 'min_t()', and introduce a type-anal 'min()' that complains about arguments of different types". The max() and min() macros were back to their old form, but the definition had changed; they now looked like:

    #define min(x,y) ({ \
	const typeof(x) _x = (x);	\
	const typeof(y) _y = (y);	\
	(void) (&_x == &_y);		\
	_x < _y ? _x : _y; })

Unsurprisingly, the complexity of these macros only grew from there as developers added more features for flexibility and type safety. Numerous variants have also been added for special cases. Recently, this series from David Laight, merged for the 6.7 kernel, made min() and max() work properly in numerous cases where the two arguments have different types. All seemed well after that, and nobody felt a compelling urge to change these macros for at least three development cycles.

Maximal expansion

But, then, Arnd Bergmann observed that the time required to compile the kernel had grown considerably in recent releases, and that the preprocessor had a lot to do with it; one file took a full 15 seconds just to get through the preprocessor stage. The problem came down to a single line of code in arch/x86/xen/setup.c:

    extra_pages = min3(EXTRA_MEM_RATIO * min(max_pfn, PFN_DOWN(MAXMEM)),
    		       extra_pages, max_pages - max_pfn);

To see how this came about, it is worth looking at the 6.10 definitions of the min() and max() macros and their variants, all of which come from include/linux/minmax.h. To start with, min3() returns the minimum of three values; its implementation is straightforward enough:

    #define min3(x, y, z) min((typeof(x))min(x, y), z)

That uses our old friend min(); indeed, it nests one min() call inside another. In 6.10, min() looks like this:

   #define min(x, y) __careful_cmp(min, x, y)

The __careful_cmp() macro tries hard to perform a type-safe comparison while evaluating the arguments only once; it also endeavors to expand to a constant expression if its arguments are constant expressions. That leads to a certain amount of complexity, implemented this way (best read from bottom to top):

    #define __cmp_op_min <
    #define __cmp_op_max >

    #define __cmp(op, x, y)	((x) __cmp_op_##op (y) ? (x) : (y))

    #define __cmp_once_unique(op, type, x, y, ux, uy) \
	({ type ux = (x); type uy = (y); __cmp(op, ux, uy); })

    #define __cmp_once(op, type, x, y) \
	__cmp_once_unique(op, type, x, y, __UNIQUE_ID(x_), __UNIQUE_ID(y_))

    #define __careful_cmp_once(op, x, y) ({			\
	static_assert(__types_ok(x, y),			\
		#op "(" #x ", " #y ") signedness error, fix types or consider u" #op "() before " #op "_t()"); \
	__cmp_once(op, __auto_type, x, y); })

    #define __careful_cmp(op, x, y)					\
	__builtin_choose_expr(__is_constexpr((x) - (y)),	\
		__cmp(op, x, y), __careful_cmp_once(op, x, y))

Depending on the expressions passed in, this means that min3() can end up generating a fair amount of code. Even if one expects a large expansion, though, the actual amount may lead to significant eyebrow elevation: the single line of code shown above expands to 47MB of preprocessor output. Bergmann explained this result this way:

It nests min() multiple levels deep with the use of min3(), and each one expands its argument 20 times times now (up from 6 back in linux-6.6). This gets 8000 expansions for each of the arguments, plus a lot of extra bits with each expansion. PFN_DOWN(MAXMEM) contributes a bit to the initial size as well.

Kernel developers, as a rule, care deeply about efficiency; that is especially true when it comes to the time required to do a kernel build. So it is unsurprising that this problem attracted some attention once it came to light.

Minimizing the problem

Lorenzo Stoakes brought the issue to the linux-kernel mailing list, showing how the 6.7 changes had made compilation time worse. Laight posted a patch series one day later that attempted to mitigate the problem. That series improved compilation time, though not enough to completely make up for the build-time regressions seen. It also ended up provoking some warnings from the test bots, and some of the changes to the macros made some developers (including Bergmann) nervous; those macros have reached a level of subtlety that makes people reluctant to change them. Torvalds, too, was uncomfortable with some of the changes, but he also wondered if they were the right approach to take in the first place:

I do get the feeling that the problem came from us being much too clever with out min/max macros, and now this series is doubling down instead of saying "it wasn't really worth it".

He later suggested simply reverting the 6.7 changes even though the previous code was "stupid and limited and caused us to have to be more careful about types than was strictly necessary" but, as Stoakes pointed out, a lot of code in the kernel has since come to depend on the new functionality that those changes added. Reverting them now would not be a straightforward task.

So Torvalds decided to take a bit of a different approach after observing that many of the worse expansion cases were, in the end, relatively simple constant expressions. Rather than try to fix the existing complex macros, he just added a couple more with a familiar look to them:

    /*
     * Use these carefully: no type checking, and uses the arguments
     * multiple times. Use for obvious constants only.
     */
    #define CONST_MIN(a,b) ((a)<(b)?(a):(b))
    #define CONST_MAX(a,b) ((a)>(b)?(a):(b))

By the time these macros landed in the mainline they had naturally gained just a little complexity (and new names):

    #define MIN_T(type,a,b) __cmp(min,(type)(a),(type)(b))
    #define MAX_T(type,a,b) __cmp(max,(type)(a),(type)(b))

He converted a number of the worst expansion cases to use the new macros just prior to the 6.11-rc1 release, then merged a patch taking away the ability for min() and max() to work as part of a constant expression. That simplified the code somewhat at the cost of making the macros unsuitable for use in places where constants are needed, but the new macros can be used instead in such situations.

These changes will not entirely resolve the problem in cases where the expressions are not constant, so chances are that more tweaks to the regular min() and max() macros are in store. Meanwhile, though, we have had a convincing demonstration of the sorts of pitfalls that can accompany this sort of extensive use of the C preprocessor. It can accomplish some magical-seeming effects, but spells of this nature often have subtle and unpleasant side effects.

Index entries for this article
KernelBuild system


to post comments

compiler magic?

Posted Aug 1, 2024 14:50 UTC (Thu) by jhoblitt (subscriber, #77733) [Link] (13 responses)

As C doesn't allow one to write polymorphic functions, I am surprised there isn't a type safe compiler extension to handle these common operations. Has this been discussed at the standards level for a future C version?

compiler magic?

Posted Aug 1, 2024 15:52 UTC (Thu) by clugstj (subscriber, #4020) [Link] (12 responses)

It's already there. It's called "_Generic"

compiler magic?

Posted Aug 1, 2024 17:17 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (4 responses)

And there's an even better one called "C++".

compiler magic?

Posted Aug 2, 2024 0:51 UTC (Fri) by DanilaBerezin (guest, #168271) [Link] (1 responses)

Probably not happening any time soon.

compiler magic?

Posted Aug 5, 2024 20:23 UTC (Mon) by cytochrome (subscriber, #58718) [Link]

We need to keep in mind what Max Planck wrote:

A new scientific truth does not triumph by convincing its opponents and making them see the light, but rather because its opponents eventually die and a new generation grows up that is familiar with it ...

compiler magic?

Posted Aug 2, 2024 15:34 UTC (Fri) by bmork (subscriber, #88411) [Link]

> And there's an even better one called "C++".

I see I'm not the only one missing the good old flame wars :-)

compiler magic?

Posted Aug 2, 2024 23:32 UTC (Fri) by iteratedlateralus (guest, #102183) [Link]

C++ has std::min()

compiler magic?

Posted Aug 1, 2024 17:20 UTC (Thu) by jhoblitt (subscriber, #77733) [Link] (2 responses)

Ha! I guess I am happily obvious to what's going on in C...

Doesn't the kernel already allow C11?

compiler magic?

Posted Aug 1, 2024 17:59 UTC (Thu) by adobriyan (subscriber, #30858) [Link]

_Generic is in vogue these days!

compiler magic?

Posted Aug 2, 2024 0:07 UTC (Fri) by gerdesj (subscriber, #5446) [Link]

"Ha! I guess I am happily obvious to what's going on in C..."

I suspect you meant "oblivious" and yet I prefer "happily obvious" ... not too sure why. Perhaps its staring me in the face.

compiler magic?

Posted Aug 2, 2024 1:00 UTC (Fri) by Keith.S.Thompson (subscriber, #133709) [Link] (2 responses)

I'm not sure how "_Generic" would be useful for min/max macros.

The macro would have to list all the types you want the macros to work with, possibly with a "default:" to handle all types you haven't specified. But how would the code differ for different types?

You could catch type mismatches, like "min(1, 1.5)", but that would require nested generic selections on both arguments.

Personally, without having studied the kernel code that uses it, I would think that something simple like:

#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define MAX(x, y) ((x) > (y) ? (x) : (y))

would be sufficient. The all-caps macro names emphasize that the arguments might be evaluated more than once and remind users to be careful. Any code that uses them could be audited (not on every build) to ensure it behaves sanely.

Quite possibly I'm missing something.

compiler magic?

Posted Aug 2, 2024 16:30 UTC (Fri) by bluss (guest, #47454) [Link] (1 responses)

I think you're right. _Generic was created for one reason, to do "type generic" math functions, and they dispatch on a single argument. That's more or less what it's good for, single argument functions of numeric types.

compiler magic?

Posted Aug 4, 2024 15:40 UTC (Sun) by fw (subscriber, #26023) [Link]

Unfortunately, nested type-generic functions are common enough that this does not work due to the same macro expansion size issue that is discussed here. Any reasonable implementation of the type-generic math macros needs to use something else. GCC has __builtin_tgmath.

compiler magic?

Posted Aug 4, 2024 15:37 UTC (Sun) by fw (subscriber, #26023) [Link]

_Generic does not help to reduce preprocessor expansion because it's necessary to reference the argument at least twice.

C++

Posted Aug 1, 2024 17:14 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (41 responses)

Maybe the kernel should reconsider C++? If GDB can do it, Linux can too. Keep the .c file extension, like GDB does, if it makes people feel better. (Also, of course, continue the Rust project.) Don't want C++ name mangling? Write a post-build pass that rewrites mangled to unmangled symbols in the symbol table when unambiguous.

C++ would allow the "shape" of the kernel to be unchanged (nobody makes you use, e.g. std::vector!) while allowing the kernel to replace macro abominations like the DEFINE_TRACE, 8000-macro-expansion max(), etc. with mere template abominations. Like mathematical infinities, some abominations are more abominable than others.

C++'s template system _comprehensively_ solves the _entire class_ of problem this article discusses, and it does so in a way that's elegant, composable, comprehensible, and safe. There is literally no C program that cannot trivially be made a C++ program.

Modern dialects (e.g. with variadic templates) so comprehensively clobbers ANSI C on safety and expressiveness without giving up an iota of performance that one struggles to imagine reasons other than mere inertia keeping C++'s safe and powerful type system out of kernel developer hands.

C++

Posted Aug 1, 2024 17:21 UTC (Thu) by mb (subscriber, #50428) [Link] (38 responses)

Yes, but the article is also about performance.
With C++ the build performance goes down the drain.

C++

Posted Aug 1, 2024 17:23 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (28 responses)

> With C++ the build performance goes down the drain.

Does it? Why would it?

C++

Posted Aug 1, 2024 17:30 UTC (Thu) by mb (subscriber, #50428) [Link] (27 responses)

Have you ever compiled a C++ file? It's dead slow compared to C. No matter what compiler is used.

C++

Posted Aug 1, 2024 17:34 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (18 responses)

Uh, no? A myth doesn't become truth just because someone repeats it emphatically enough.
$ time for ((i=0; i < 100; ++i)); do gcc -O2 foo.c; done

real	0m6.669s
user	0m4.412s
sys	0m2.237s

~
$ time for ((i=0; i < 100; ++i)); do gcc -O2 -x c++ foo.c; done

real	0m6.995s
user	0m4.677s
sys	0m2.302s

$ cat foo.c
#include <stdio.h>

int
main()
{
  printf("hello world\n");
  return 0;
}

C++

Posted Aug 1, 2024 17:59 UTC (Thu) by mussell (subscriber, #170320) [Link] (17 responses)

That isn't C++ code, that is C code passed through a C++ compiler that uses nothing more than function calls. Once you start using templates like std::cout (which is what every C++ tutorial uses), compilation suffers dramatically as template instantiation can create the equivalent of thousands of lines of code, all for it to be deleted by a later compiler pass. This issue isn't unique to C++ as Rust uses the same technique, and as this article has shown, the same can happen with C macros.

A quick check on my system shows that the idiomatic C++ code

#include <iostream>

int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}

takes about 9x as long to compile using G++ compared to your idiomatic C code in GCC (0.27s vs 0.03s). Even building the C code in G++ takes 0.07s which is twice as long as GCC and about the same time as clang (not clang++).

C++

Posted Aug 1, 2024 18:05 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (13 responses)

> That isn't C++ code, that is C code passed through a C++ compiler that uses nothing more than function calls.

It's C++ code that also happens to be valid C code. Compiling it as C++ is just as fast as compiling it as C. This result refutes mb's claim that compiling C++ is necessarily slow. There are more nuanced points to be made, as you do, but mb is dead-to-rights wrong.

> Once you start using templates like std::cout (which is what every C++ tutorial uses), compilation suffers dramatically as template instantiation can create the equivalent of thousands of lines of code, all for it to be deleted by a later compiler pass.

And if you instantiate a C macro 8,000 times, performance suffers. Nobody is forcing you to use <iostream> or std::cout just because you're writing C++

> idiomatic C code in GCC (0.27s vs 0.03s)

The Linux kernel isn't "idiomatic" C. Why should it be "idiomatic" C++? If the kernel can avoid massive macro expansions, it can avoid massive template instantiation too. My point is that both macros and C++ metaprogramming generate code, but the latter is a safer and more concise way to get to the same endpoint.

There's nothing *inherent* in C++ that turns tight C programs into bloated Boost-Spirit-style masses of template expansions and specialization, no more so that C inherently turns programs into mazes of CPP macros.

C++ template is a better meta-programming system than C macros, hands down. Neither language makes metaprogramming mandatory.

C++

Posted Aug 1, 2024 18:12 UTC (Thu) by mb (subscriber, #50428) [Link] (12 responses)

>This result refutes mb's claim that compiling C++ is necessarily slow.

Not true.

If all you have is C code, why on earth do you want to compile it with a C++ compiler?
Of course you would have C++ code (templates) in there.
That was *your* point. You want to replace C macros with C++ templates.

C++

Posted Aug 1, 2024 18:36 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (11 responses)

C++ makes you pay only for what you use. Why would a simple template be more expensive to instantiate than a simple preprocessor macro would be to expand? It's not as if C++ has only two mods: 1) C, and 2) Boost. There are plenty of useful intermediate points.

C++

Posted Aug 1, 2024 18:47 UTC (Thu) by mb (subscriber, #50428) [Link] (7 responses)

Well, the simple fact that all existing C++ projects are slow to build.
People use the features that are available.

That's why they abuse the C preprocessor to the max (hehe) and that's why C++ features will be used, if available.
That is the whole point of compiling with a C++ compiler.

Show me a single C++ project that builds as fast as a comparable C project.

C++

Posted Aug 1, 2024 18:49 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (4 responses)

The same argument applies to Rust, which if anything is even worse than C++ on monomorphization bloat potential, yes? Why worry about people abusing C++ metaprogramming but not Rust metaprogramming?

C++

Posted Aug 1, 2024 18:51 UTC (Thu) by mb (subscriber, #50428) [Link] (3 responses)

Yes, Rust has the same compilation speed problems.

C++

Posted Aug 2, 2024 19:53 UTC (Fri) by walters (subscriber, #7396) [Link] (2 responses)

One thing that is definitely relevant and nice about Rust here though is that if you do e.g. `const FOO` you are guaranteed constant-time evaluation in an efficient way.

And some of the major offenders seemed to end up being constant values:

https://lwn.net/ml/all/CAHk-=wjPr3b-=dshE6n3fM2Q0U3guT4re...

```
#define pageblock_order min_t(unsigned int, HUGETLB_PAGE_ORDER,
MAX_PAGE_ORDER)
```

That could be `const PAGEBLOCK_ORDER: u32 = ...`...except yes, I don't think there's any way yet to do min/max() (i.e. in a generic fashion) yet in Rust...blocked on "const fn in traits" https://github.com/rust-lang/rfcs/pull/2632 I think? But one can trivially define a `const fn` for it and use it like:

```
mod someotherlib {
pub(crate) const FOO: u32 = 42;
}

const fn min_u32(a: u32, b: u32) -> u32 {
if a < b {
a
} else {
b
}
}

const BASIC_VAL: u32 = 20;
const SOMEVAL: u32 = min_u32(someotherlib::FOO, BASIC_VAL);
```

There may even be a crate for `const fn` variants like that although it may be getting somewhat "left-pad" territory.

(And maybe I am missing a clever way to use the more elegant, generic and Rust-native Ord trait https://doc.rust-lang.org/std/cmp/trait.Ord.html#method.min in const expressions).

C++

Posted Aug 2, 2024 20:03 UTC (Fri) by adobriyan (subscriber, #30858) [Link]

__inline constexpr auto pageblock_order = min<unsigned int>(hugetlb_page_order, max_page_order);

C++

Posted Aug 5, 2024 14:02 UTC (Mon) by cencer (subscriber, #40823) [Link]

FWIW C23 added support for constexpr objects https://open-std.org/JTC1/SC22/WG14/www/docs/n3018.html

C++

Posted Aug 4, 2024 5:04 UTC (Sun) by ssmith32 (subscriber, #72404) [Link] (1 responses)

No. That's been proved wrong above.

If you're making a claim about *all* C++ projects, then, if there exists one C++ project that contradicts your point, you are *wrong*.

So, you're wrong. QED.

You could try a little nuance and subtlety...

C++

Posted Aug 4, 2024 8:20 UTC (Sun) by mb (subscriber, #50428) [Link]

Yes, you are right.
A completely made up nonsense project that only exists to prove that I am wrong builds as fast as C.
Any other real world projects builds slowly.

C++

Posted Aug 3, 2024 10:34 UTC (Sat) by marcH (subscriber, #57642) [Link] (2 responses)

> It's not as if C++ has only two mods: 1) C, and 2) Boost. There are plenty of useful intermediate points.

and _that_ is the problem with C++. Which intermediate point do you allow in your project? No one can tell and endless debates ensue.

For instance, everyone agrees that "modern" C++ is much more memory safe than C[*]. But what exact "intermediate point" is "modern"? Which "old" and unsafe features are now forbidden and must be replaced in everyone's code base at great re-validation expense? No one knows; not even Stroustrup: https://thenewstack.io/bjarne-stroustrups-plan-for-bringi... By the time C++ experts have agreed on that, even Rust will be obsolete and replaced.

This is what happens when you have too many cooks.

[*] memory safety is not very important but it's a fashionable topic nowadays /s

C++

Posted Aug 3, 2024 12:20 UTC (Sat) by malmedal (subscriber, #56172) [Link] (1 responses)

Herb Sutter has made a proposal that you can actually try yourself: https://hsutter.github.io/cppfront/ I find it better than C++, but unsure if it is enough better to be worth the hassle.

C++

Posted Aug 3, 2024 16:18 UTC (Sat) by marcH (subscriber, #57642) [Link]

Interesting! I sincerely hope the C++ community eventually converges on one of these approaches that eventually wins over all the others and that one day every project can just check a box to add a safety check CI that rejects "old" C++. As long as you don't have such a simple level of automation, "modern C++" will keep being a science experiment. Sutter's proposition looks like it's ready for that, nice! Now it just needs to win :-)

I don't know whether it will help with other C++ issues (e.g.; Perl-like, "write-only" possibilities) but having a memory safe C++ would be great for humankind. No, I'm not exaggerating: think about the gazillions of C++ lines the world depends on.

Besides being a great language in itself, I think Rust has created a lot of pressure for C++ to (literally) clean up its act. Competition is good! Discussions about languages tend to be passionate, especially on "asocial" media but it would be great for the real world to end up with TWO good alternatives.

You just don't want "too much" competition and too much fragmentation either, especially not with programming languages: there's no room for 10 different C++ variants - which is the problem for now. Funny for a language to be so old and so experimental and immature at the same time.

C++

Posted Aug 2, 2024 21:40 UTC (Fri) by magfr (subscriber, #16052) [Link] (2 responses)

That is not the recommended way to write Hello World! in C++ any more. The new fancy way as of C++23 is
#include <print>

int main()
{
  std::println("Hello World!");
}

C++

Posted Aug 3, 2024 2:39 UTC (Sat) by NYKevin (subscriber, #129325) [Link]

Oh thank ****, they're finally getting rid of iostream?

C++

Posted Aug 3, 2024 9:47 UTC (Sat) by excors (subscriber, #95769) [Link]

That's based on the {fmt} library (https://fmt.dev), which has a basic benchmark at https://github.com/fmtlib/fmt?tab=readme-ov-file#benchmarks . fmt::print() is 5x faster to compile than libc++ iostreams, with half the executable size, and 3x faster run-time. Compared to libc printf it's 3x slower compile time, same executable size, and 20% faster run time (it sounds like most of the difference is in formatting floats).

If you're not using C++20/23, you should probably use {fmt} anyway - I think it's an almost entirely compatible superset of the new std::format/std::print APIs, and it only requires C++11, and it's better than iostreams in every way.

C++

Posted Aug 1, 2024 18:02 UTC (Thu) by wtarreau (subscriber, #51152) [Link] (7 responses)

It's even more visible on any project that combines both. "make" scrolls fast for a while and suddenly comes to a halt, and when you look at the last two lines, the first one starts with gcc and the second with g++. Anyone who builds libs regularly is used to seeing this!

C++

Posted Aug 1, 2024 18:17 UTC (Thu) by adobriyan (subscriber, #30858) [Link] (6 responses)

Linux++ needs

a) modules -- Microsoft claims importing _all_ of std is faster then _including_ just some headers (?) (<vector> ?)
this solves (in theory) compile time degradation (which exists, Linux++ compiles noticeably slower than Linux)

b) gcc needs to support C99 initializers in their full glory:
forcing devs to place initalizers in order is downgrade in kernel programming experience.
Clang can do it. See : https://gcc.gnu.org/bugzilla/show_bug.cgi?id=113124

c) tell clang devs that arithmetic on "void*" is fine, so both compilers can be supported

gcc has -Wno-pointer-arith and Universe didn't collapse.

Tell both groups that "void* - void*" is fine!

d) deal with RESF
does Rust support extern "C++" and what else is necessary? I don't know.
Presumably interop goes via extern "C" but (!) all of kernel becomes extern "C++" and they want to interop potentially with _all_ kernel subsystems!

Everything else is minor nuisance in comparison.

Except implicit casts from void* to T* which is non issue but undoing 25 years of removing efforts in 1 commit... Oh.

C++

Posted Aug 1, 2024 23:53 UTC (Thu) by NYKevin (subscriber, #129325) [Link] (5 responses)

I was going to say "no, of course Rust doesn't do extern C++, nobody does that" - but it turns out I'm wrong and they do have extern "thiscall", which is Microsoft for "C++ but only if you use MSVC's mangling (which is also supported by GCC and Clang via __attribute__() syntax)." Unfortunately, according to MSDN:

> On ARM, ARM64, and x64 machines, __thiscall is accepted and ignored by the compiler. That's because they use a register-based calling convention by default.

As far as I can tell, that works out to thiscall being usable on every platform *except* (modern) Windows, which is mildly amusing (and presumably not an issue for Linux, but IDK maybe it gives the WSL people a headache).

C++

Posted Aug 2, 2024 0:57 UTC (Fri) by DanilaBerezin (guest, #168271) [Link] (4 responses)

> but IDK maybe it gives the WSL people a headache

WSL is an actual virtual machine. So AFAIK it shouldn't.

C++

Posted Aug 2, 2024 9:58 UTC (Fri) by aragilar (subscriber, #122569) [Link]

WSL2 is a virtual machine (with a bespoke kernel), WSL1 is not (which I guess is what the parent is referring to?). I'm not sure if anyone is using WSL1 though still (I've heard it's better for some things such as how access to the Windows file system is provided than WSL2).

WSL

Posted Aug 2, 2024 9:59 UTC (Fri) by rschroev (subscriber, #4164) [Link] (2 responses)

To avoid any confusion: WSL 2 is a virtual machine. WSL 1 isn't; it's a compatibility layer, with the Windows kernel implementing a large subset of Linux system calls.

"WSL is an actual virtual machine" is probably mostly true: I think WSL 2 is used much more than WSL 1.

WSL

Posted Aug 2, 2024 10:35 UTC (Fri) by WolfWings (subscriber, #56790) [Link]

WSL1 needs to be explicitly enabled at this point on any recent version of Windows freshly installed, WSL2 is the only supported out-of-the-box model by default now. So the defaults available for WSL installs realistically today? Yes, it's all virtualization.

But also a shockingly convenient setup since you can still run Windows binaries inside the WSL environment as well since it's also all just a local virtualized CIFS mount into all the VM filesystems.

WSL

Posted Aug 2, 2024 20:49 UTC (Fri) by Heretic_Blacksheep (guest, #169992) [Link]

https://learn.microsoft.com/en-us/windows/wsl/compare-ver...

"WSL is an actual virtual machine," as far as most people refer to it is not wrong. Almost no one is referring to WSL1 these days and they'll usually remember to specify if they are.

C++

Posted Aug 1, 2024 18:01 UTC (Thu) by adobriyan (subscriber, #30858) [Link] (8 responses)

Plot twist: C++ min(a, b) turns out to be faster than macroexpanding C min/max macros.

C++

Posted Aug 2, 2024 22:51 UTC (Fri) by wahern (subscriber, #37304) [Link] (7 responses)

The problematic macro mess does more than 2-argument std:min. The earlier version,

#define min(x,y) ({ \
	const typeof(x) _x = (x); \
	const typeof(y) _y = (y); \
	(void) (&_x == &_y); \
	_x < _y ? _x : _y; })
is the std:min equivalent, and should be at least as easy on the compiler. (That said, while C23 has typeof, statement expressions are still non-standard.) A trivial min3 could be written using the same typeof trick. The newer min3 and min rely on __careful_cmp, which (among other features) is apparently intended to safely permit comparison of integers of different type, similar to the classic MIN macro, but without the signed -> unsigned conversion pitfall which can happen with integral promotion at the highest integral rank.

Disclaimer: I've not done much C++, but looking at the template interface, and testing some sample code, AFAICT std:min requires integers of the same type. Please correct me if I'm wrong. I would imagine any solution that permits safely and optimally comparing differently typed integers is going to be messy in most languages. If it's not messy--or at least verbose--it's not likely going to produce the most efficient machine code. It would be interesting to compare an equivalent C++ template solution in terms compiler memory and CPU cost. I wouldn't hazard a guess, and wouldn't be surprised by any particular result.

C++

Posted Aug 3, 2024 10:02 UTC (Sat) by roc (subscriber, #30627) [Link] (4 responses)

The point of this article is that that definition of `min` is not really easy on the compiler because it expands x and y twice each, and if you use enough nesting you get exponential blowup in the size of the preprocessed code. Yes, a lot of that code is inside `typeof` so is less work for the compiler than generating code to evaluate the expression's value, but generating and then parsing a huge amount of preprocessed code is still a problem.

C++

Posted Aug 3, 2024 21:58 UTC (Sat) by wahern (subscriber, #37304) [Link]

I was replying to a tongue-in-cheek comment about C++ min (I assumed std:min) being faster than the min version discussed in the article, and was just pointing out that the problematic min version in the article isn't comparable to C++ std:min; that the comparable version is the *older*, simpler min (the one I pasted into my comment). A C++ version with semantics similar to the problematic C preprocessor version would probably require some template meta programming--i.e. similar recursion. Whether it would induce as much memory allocation, I don't know. Maybe not.

Given that we're only dealing with integral types, and any type narrower than an int will be promoted to an int--reducing the set of possible permutations--one wonders whether some manual enumeration might help. Perhaps there are issues with qualified types and otherwise coercing the compiler to match arguments to a small enough set of _Generic cases? Maybe this is where C++ would shine, with a well defined set of template type matching rules, plus constexpr. (GCC provides constexpr for C code, but only in very recent versions, in anticipation of constexpr in C23.)

C++

Posted Aug 5, 2024 10:13 UTC (Mon) by kleptog (subscriber, #1183) [Link] (2 responses)

With GCC you can use __auto_type to avoid the double expansion. It's even used as an example for max() in the GCC docs. [1]

[1] https://gcc.gnu.org/onlinedocs/gcc/Typeof.html

C++

Posted Aug 5, 2024 12:54 UTC (Mon) by Wol (subscriber, #4433) [Link] (1 responses)

What's the betting the definition of "max" is older than __auto_type. It's all very well all these fancy features, but how often are they closing the stable door after the horse has bolted (and no-one realises there should be a horse inside that particular stall ...)

Cheers,
Wol

C++

Posted Aug 5, 2024 16:25 UTC (Mon) by kleptog (subscriber, #1183) [Link]

Oh never mind. If you look at the macros in the article, you'll see an __auto_type buried in there, so they are aware of its existence. Apparently, they want sufficiently more magic that its doesn't save them in this case.

C++

Posted Aug 3, 2024 23:13 UTC (Sat) by NYKevin (subscriber, #129325) [Link] (1 responses)

After experimentation, it looks like you're right that std::min() does protect the type of its arguments, but it does not prevent implicit coercion of its return value. The following compiles (with -Wall -Werror) and outputs -2 (on platforms where a byte is 8 bits, which is most of them):

#include <algorithm>
#include <iostream>

char find_min(unsigned char a, unsigned char b){
return std::min(a, b);
}

int main(){
std::cout << int(find_min(255, 254)) << std::endl;
}

To be fair, that's not really something that std::min() is capable of fixing, nor is it specific to C++ (C will do exactly the same thing, given the opportunity). I personally see this as one of C and C++'s cardinal sins, but it's not a reason to prefer one over the other.

(No this is not UB, according to https://en.cppreference.com/w/cpp/language/implicit_conve... it is implementation-defined before C++20 and wrapping after.)

C++

Posted Aug 20, 2024 7:29 UTC (Tue) by daenzer (subscriber, #7050) [Link]

FWIW, your example works only on platforms where char is signed by default (or if built with -fsigned-char), which it isn't for all Linux ABIs. Can make it work universally by changing find_min's return type to "signed char".

C++

Posted Aug 2, 2024 23:57 UTC (Fri) by iteratedlateralus (guest, #102183) [Link] (1 responses)

Maybe the kernel should reconsider C++?

I think the major hurdle there is to have Linus accept C++ as a language he wants to use. He is not a fan of the language.

C++

Posted Aug 3, 2024 16:00 UTC (Sat) by Sesse (subscriber, #53779) [Link]

The kernel has made its choice for C replacement, and that choice is quite clearly Rust.

(I'm not saying the answer is right, nor wrong)

Classification as...

Posted Aug 2, 2024 23:31 UTC (Fri) by iteratedlateralus (guest, #102183) [Link]

would this be considered technical debt?

What a great story

Posted Aug 9, 2024 16:32 UTC (Fri) by mbp (subscriber, #2737) [Link]

Thanks Jonathan!


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds