4 ms·
> Inlining, tailcall, hot/cold: None of these are issues. They don't change the fact that memory passed to gc() will be freed. Worst case scenario is the can ge
by comex 6y ago
> Inlining, tailcall, hot/cold: None of these are issues. They don't change the fact that memory passed to gc() will be freed. Worst case scenario is the can gets kicked down the road, which is relatively easy to predict. [..] So long as you're not using this technique to unlock mutexes, you'll be fine.
That's true for inlining and tailcall, but not hot/cold, since it can make cleanups execute earlier than expected rather than later. This happens if: (1) some chunk of the function is extracted into a separate function; (2) the chunk contains a call to defer; and (3) some code which is not in the extracted chunk, but executes after it, expects the cleanup to not have run yet. See this example:
https://gcc.godbolt.org/z/1ro1z6 https://gcc.godbolt.org/z/1ro1z6
To be fair, Clang does not enable this optimization by default, and it might get replaced by a different implementation of hot-cold splitting [1] which happens to not suffer from this issue (because it operates later in the pipeline, essentially splitting blocks at the assembly level rather than the IR level).
On the other hand, GCC does enable a form of hot-cold splitting by default, via -fpartial-inlining, but it's limited to splitting out suffixes of the function (i.e. regions starting somewhere in the function and including everything in the function that can be executed from then on), rather than arbitrary regions. Therefore, it can't run into the problematic case where non-extracted code runs after extracted code. Still, this is just an implementation limitation that could be lifted in the future.
> I like the gc() macro because it can be used in expressions. I find __attribute__((__cleanup__)) unpleasant since it has strong opinions about how variables and cleanup functions need to be declared.
Fair enough; I do agree on that point. (I wish GCC had a way to either use attribute cleanup with C99 compound literals, or somehow declare variables that live within statement expressions as having their lifetime extended to a surrounding block… Maybe what I really want is a better macro system. Or the native `defer` feature proposed here, but I doubt that will ever happen.)
[1] https://lists.llvm.org/pipermail/llvm-dev/2020-August/144012.html https://lists.llvm.org/pipermail/llvm-dev/2020-August/144012...
- jart 6y agoPlease note the intended use case was this: #define gc(x) ({ auto y=x; defer(free, y); y; }) int *buf = gc(malloc(sizeof(int))); if (coldpath) { ... It's a bit of stretch to leak memory doing this: int *buf = malloc(sizeof(int)); if (coldpath) { defer(fake_free, buf); ... Your point is otherwise valid. It appears a nonstandard nondefault LLVM extension can break the macro under circumstances that are difficult to imagine happening in practice. Here's why LLVM is wrong. It surgically removes a chunk of code from the middle of a function, turn it an external function, and then emits a synthetic call. It should jump to the cold block and jump back. The amount of code I'm seeing it generate just to handle the abi boundary it needlessly created is almost as large as the chunk that's being outlined. Godbolt's website doesn't show this, but if you pass -S to clang-10 you'll notice other suboptimalities exist in the way this LLVM extension was implemented. For example, it doesn't emit .section directives to relocate the cold code, which is half the point of hot cold pgo style optimizations, since I recall Google building the thing to not have their web-scale static binaries paging hundreds of megs of cold code off disk. LLVM is doing great work, but still has so much catching up to do compared to GCC on the code generation. GCC 9+ does cold optimizations by default, but only to relocate cold noreturn error handling paths into .text.unlikely. That's not an issue. I can't say for certain what it does if you generate and pass and optimization profile.