![]() |
|
![]() |
| >[[musttail]] also ends the lifetime of local objects AFAICS.
That's good to know. I had this github issue [0] in the back of my mind, as well as witnessing occasions of clang turning [[musttail]] into inner loops, and concluded clang's implementation must be more sophisticated than simply replacing calls with jumps. Just a little paranoia from trying to be serious with compiler dev[1]: fulfilling a laid-out spec feels more sound versus imitating something out there. >this constraint is unnecessarily strict I would agree, at least for x86 psABI, it can be pretty elaborative as long as the return value is the same register and argument stack don't exceed what's provided. Debug/profiling side might hate it, though. [0] https://github.com/llvm/llvm-project/issues/72555 [1] https://github.com/fuhsnn/slimcc/ |
![]() |
| > some targets fundamentally do not support tail call optimization
Can't any tail call be rewritten as a loop? Couldn't a WASM compiler without the tail call extension implement it this way? |
![]() |
| More specifically, tail recursion is usually easy to turn into a loop. Tail calls can be difficult to turn into loops when they call a different function, or worse a function passed in as a variable. |
![]() |
| What new energy? We have had C89, C99, C11, C17, C23
And yet, no fix in sight for proper strings, arrays, or at very least bounds checked slices, like Dennis Ritchie originally proposed to ANSI/ISO. |
![]() |
| I’ve heard this went out of fashion a while back. That branch predictors got good enough that it’s not necessary anymore.
But I wonder if that stays true as the size of the interpreter increases. |
![]() |
| My most recent data says it's still relevant.
It might not matter for very small interpreters, but it does matter for anything substantial. Definitely worth remeasuring though. |
![]() |
| preserve_none is the new one. It can be applied to the functions performing tail calls to allow them use of the full register space.
I even saw an enhancement recently that will make preserve_none allocate arguments in the registers that are usually callee-saved: https://github.com/llvm/llvm-project/pull/88333 This will make [[musttail]] + preserve_none a winning combination when used together, particularly when making non-tail calls to fallback functions that use a regular calling convention, because all the arguments to [[musttail]] functions can stay pinned in the callee-save registers. I'm delighted, because this matches what I originally proposed back in 2021, except I called it "reverse_ccc" instead of "preserve_none": https://github.com/haberman/llvm-project/commit/e8d9c75bb35c... preserve_all also exists, and has existed for a while. You could use it on fallback functions to help the tail calling functions avoid spilling. But this always seemed like an unsatisfactory solution to me, because it's too intrusive (and requires too much diligence) to tag a bunch of regular functions as preserve_all. It's much more practical to tag all the core interpreter functions as preserve_none. |
![]() |
| Since preserve_none is Clang-only and only available on recent compilers, it would introduce an ABI hazard between the core library and generated code. We don't want to cause crashes if you compile protobuf with one compiler and generated code with another.
Also, until quite recently preserve_none was incompatible with the sanitizers, but I believe this may have been fixed by: https://github.com/llvm/llvm-project/pull/81048 |
![]() |
| What about the preserve_most attribute? Is there any chance something like that will get into GCC? Without it, the non-tail calls ruin the interpreter. |
![]() |
| Maybe, but not there yet: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=110899
In the meantime some inline assembly macro trickery might help. edit: code duplication can also be obviated by templating your op function with a fast/slow parameter, with the fast variant tail-calling the slow variant when it cannot perform the fast path, while guarding the slow code via the compile time parameter. The downside is yet more code obfuscation of course. |
![]() |
| It's a hard problem because many ABIs cannot do tail calls even for very basic stuff, like calls to extern functions with matching argument and return types. It looks like Clang has some heuristics for changing call sequences for musttail calls. For example, it switches to noplt calls on i686. None of this is mentioned in the Clang documentation: https://clang.llvm.org/docs/AttributeReference.html#musttail
What's realistic here is that you get a diagnostic if the compiler cannot generate a tail call. For many users, that will likely be good enough. Guaranteeing a tail call as in Scheme is unlikely to happen. |
![]() |
| That means that any such code is not portable across compilers anymore. It is effectively written in a non-standard C dialect, because it requires a language extension to work correctly. |
![]() |
| Loops are just a special, limited case of recursion.
(And only necessary in languages that have trouble implementing function calls properly.) |
![]() |
| Yes, that is correct. You cannot do this trick in standard C, C++ or Rust, it requires some version of [[musttail]]. Strong argument for adding it to the C standard, IMHO. |
![]() |
| > so we want a way to tell them to not worry about the ABI and avoid wasting time on preserving registers across the call
that's what -fvisibility=internal already does, no? |
![]() |
| I wonder how fast this would be when using a trampoline, i.e. returning the next function as a function pointer and calling that from an outer loop. That would have the advantage of being portable C. |
![]() |
| C is often used as target language for compiler from higher level languages.
The Scheme programming language requires all tail calls not to grow the stack.
Therefore implementors have explored various techniques including trampolines.
I don't have a citation, but you can find the answer in the papers on compiling Scheme to C. If there is no guarantee of TCO in the target language, then the generated programs will be slower.
Incidentally, this is why implementors of (especially) high level languages are annoyed that TCO was removed from the JavaScript specification. There are even solution for having TCO and still have stack inspection. https://github.com/schemedoc/bibliography/blob/master/page8.... |
![]() |
| If added to Clang then I would assume this means it got support from LLVM. If so, does this mean that Rust can now rely on tail calls in LLVM to support something like the `become` keyword? |
![]() |
| The problem with doing it in rust is that most calls aren't tail calls, even if they look like tail calls. You need to invoke the destructors for any code path that can drop. |
![]() |
| Isn't that the purpose of `become`? I thought it was to say "this IS a tail call, error out if it is not". After that validation is done, then the compiler can drop as needed. |
![]() |
| The musttail return is not allowed to occur within a try block, or within the scope of variables with nontrivial destructors [0]. (In particular, this means that the function parameters cannot have nontrivial destructors.) Otherwise, within a musttail-called function, it's as if the original caller directly called that function, so exceptional stack unwinding is unaffected.
[0] https://www.mail-archive.com/[email protected]/msg95265.html |
![]() |
| I wonder how this behaves in combination with __attribute__((cleanup(...))). Especially if the to be cleaned variable is passed into the tail function as parameter. |
![]() |
| > In general case, Protobuf parsing cannot be streamed (because of the handling of duplicates)
I don't see how last field wins stops you from streaming parse. Please elaborate. |
![]() |
| It is not terribly hard to find some library for data structures in C. What I do not like about Rust is: syntax, complexity, long compile times, cargo, ... |
![]() |
| C is great and I definitely recommend learning it & becoming proficient in it. I don't think I would recommend it for a new project, for all the reasons jerf mentions in a sibling comment[1]. But there's a ton of existing code out there, and I think it provides a nice little peek into what the hardware is actually doing that you don't get from higher level languages.
> I think the work on C23 means people are starting to want to see innovation in the language again. I have to admit this makes me nervous. Part of what's nice about C is its simplicity and its stability. It's relatively easy to write a compiler, and the lack of having many different ways to do the same thing means all code written in C tends to "feel" roughly the same (outside some domain-specific areas), which means it's easy to dive into a new codebase and get up to speed. It's basically all structs & functions and abstractions built upon them. That's it. While I definitely am not opposed to making improvements to the C spec, seeing the inline anonymous functions in a proposal linked elsewhere in this thread[2] really turns me off. It starts to feel like the hideous mess that C++ has turned into, with oodles of implicit behavior, un-greppable code, generated symbol names leading to compiler error hell, and a thousand different colors for your bike shed. We already have languages that can do all this stuff. I welcome making changes to C to make it nicer to program in C, but I'm wary of proposals that turn C into yet another messy kitchen sink programming language. Let's keep it clean, obvious, and simple, please. [1] https://news.ycombinator.com/item?id=41291206 [2] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3266.htm#u... |
![]() |
| Easy, the direction WG14 is going for, is "C++ without classes, templates, improved memory safety".
Already there you can guess what features are on WG14 roadmap without going to the ISO mailings. |
![]() |
| 1. I don't think the presence of centralized package management makes or breaks a language for most people.
2. Your platform's package manager should have plenty of C packages. |
![]() |
| I think the success of Javascript and Rust certainly tests your biases.
I can say anecdotally that the lack of a standard package manager in C and C++ kept me from exploring those languages. |
![]() |
| New account, banal observation, invitation to interact. It's a pattern I've seen recently on what are apparently LLM-powered engagement bots on Twitter. |
![]() |
| It's a harmless comment that could even generate some interesting responses. I think the bar for accusing other commentators of being LLMs should be set higher than this. |
Can you elaborate on how "return goto" is easier to implement? [[musttail]] also ends the lifetime of local objects AFAICS.
One thing I'll mention from a quick scan:
> [The] function called in tail position must have identical type to the callee. This ensures both that the return value does not require any conversion, and also that argument passing space is available and calling convention (if relevant) is maintained.
One complaint I've seen repeatedly about [[musttail]] (which I implemented in Clang) is that this constraint is unnecessarily strict, since some architectures will allow tail calls even for functions that do not perfectly match: https://github.com/llvm/llvm-project/issues/54964
"But then the code would be nonportable." True, but tail call optimization is inherently nonportable, since some targets fundamentally do not support tail call optimization (eg. WASM without the tail call extension).