I don’t buy that adding an extension that is safe if you use it will move the needle. But making the language safe wholesale is practical. We should do that instead.
I don’t buy that adding an extension that is safe if you use it will move the needle. But making the language safe wholesale is practical. We should do that instead.
Why?
> We don't need that
You do if you want comprehensive use-after-free protection.
> and the minimal GC support that was in the standard has been removed from C++23.
Not related to what I'm doing. The support you cite is for users of the language to write garbage collectors "on top" of the language. Fil-C++'s garbage collector is hidden in the implementation's guts, "below" the language. Fil-C++ is compliant to C++ whether C++ says that GC is allowed or not.
I'm not yet sold on Rust, but exploring alternatives for achieving memory safety without needing to put on a GC is commendable.
That's not at all what Fil-C's garbage collector does.
If you free() an object in Fil-C, then the capability is marked free and the next GC cycle will:
- Delete the object.
- Repoint all capabilities that referred to that object to point to the free singleton capability instead.
This ensures that:
- Freeing an object really does free it, regardless of whether the GC sees it as reachable.
- Using an object after freeing it is guaranteed to trap with a Fil-C panic, and that panic is guaranteed to report that the object has been freed so you know why you're trapping.
Also, as a bonus, if you don't ever free your objects, then the Fil-C GC will delete them for you once they become unreachable. So, you can write Java-style code in C++, if you want.
> That means some objects will live for longer than you initially thought they would, and potentially even introduce circular references.
No idea what you mean there. Maybe you're thinking of reference counting, which isn't a form of garbage collection. (Reference counting is different precisely because it cannot handle cycles.)
> unpredictable slowdowns in your application to run their algorithms.
Fil-C's garbage collector is 100% concurrent. It'll never pause your shit.
However, Safe C++ (Circle) and Rust do much more than that. They are not limited to pointers on the heap, and the borrowing rules work for all references including the stack. They also work for references that need to be logically short even when the data is not freed, e.g. internal references to data protected by a mutex don't outlive unlocking of the mutex. And all of that is at zero runtime cost, and by guaranteeing the code correctly doesn't create dangling references in the first place, not merely by softening the blow of run-time failures of buggy code.
Yes, it handles references to the stack. Misuse traps or leads to other safe outcomes.
Fil-C makes it so races have memory safe outcomes (like Java).
Circle and Rust are strictly less safe than Fil-C, since both have unsafe escape hatches. Fil-C doesn't even have an unsafe escape hatch.
This can't be dismissed as a mere quality-of-implementation detail. C and C++ are used because they don't have such overheads, so it takes away the primary reason to use these languages. When non-negligible overhead is not a problem, there are plenty of nicer languages to choose from for writing new code.
This leaves Fil-C in a role of a sandbox for legacy code, when there's some unsafe C code that won't be rewritten, but still needs to be contained at any cost. But here you need to compete with WASM and RLBox which have lower-overhead implementations.
I think the reason why folks end up using C is often because they have a gigantic amount of C code and for those folks, Fil-C could easily be good enough as is.
But dismissing it as a contender because the perf isn’t there today even as it’s getting steadily faster (on a super aggressive trajectory) is a bit unfair, I think.
If you get below 2x, you can compete with WASM and ASAN. If you get it down to 1.1x-1.2x, you can compete with RLBox. If you get down to 1.05 you can call it software-emulated CHERI and kill the whole hardware line before it even comes out.
If you get it down to 1.01x, Rust will copy you, and then beat you by skipping checks on borrowed references ;)
I'm at 1.5x for a lot of workloads already. I will be solidly below 2x for sure, once I implement all of the optimizations that are in my near-term plan.
Wasm and asan don't really give you memory safety. Wasm is a sandbox, but the code running within the sandbox can have memory safety bugs and those bugs can be exploited. Hackers are good at data-only attacks. Say you run a database in wasm - then a data-only attack will give the bad guy access to parts of the database they shouldn't have access to. Fil-C stops all that because Fil-C makes C memory safe rather than just sandboxing it.
Asan also isn't really memory safe; it just catches enough memory safety bugs to be a useful tool for finding them. But it can be sidestepped. Fil-C can't be sidestepped.
So, even if Fil-C was slower than wasm or asan, it would still be useful.
> If you get it down to 1.1x-1.2x, you can compete with RLBox.
RLBox is like wasm. It's a sandbox, not true memory safety. So, it's not a direct competitor.
That said, I think I'll probably land at about 1.2x overhead eventually.
> If you get down to 1.05 you can call it software-emulated CHERI and kill the whole hardware line before it even comes out.
I can already kill CHERI because Fil-C running on my X86-64 box is faster than anything running on any CHERI HW.
No, seriously.
The biggest problem with CHERI is that you need high volume production to achieve good perf in silicon, so a SW solution like Fil-C that is theoretically slower than a HW solution is going to be faster than that HW solution in practice, provided it runs on high volume silicon (Fil-C does).
I think I'm already there, honestly. If you wanted to run CHERI today, you'd be doing it in QEMU or some dorky and slow IoT thing. Either way, slower than what Fil-C gives you right now.
> If you get it down to 1.01x, Rust will copy you, and then beat you by skipping checks on borrowed references ;)
Rust is a different animal. Rust is highly restrictive due to its ownership model, in a way that Fil-C isn't. Plus, there's billions of lines of C code that ain't going to be rewritten in Rust, possibly ever. So, I don't have to be as fast as Rust.
I don't think there is going to be much copying going on between Rust and Fil-C, because the way that the two languages achieve memory safety is so different. Rust is using static techniques to prevent bad programs from compiling, while Fil-C allows pretty much any C code to compile and uses static techniques to emit only the minimal set of checks.
C++ is usually used when people care about performance, and a GC interferes with that.
The GC is similar to what I used here, just much more optimized: http://www.filpizlo.com/papers/pizlo-eurosys2010-fijivm.pdf
Something to consider is that Fil-C permits you to do all of the things you would normally do, as a C programmer, to reduce or eliminate allocation - like having structs embedded in structs and avoidance of allocation in core algorithms.
This makes it quite different from Java or JS where the language forces you to allocate to basically do anything. I think folks conflate “GC overhead” with the overhead of languages that happen to use GC.
That is fair to a point. Some GC languages (like Java) are significantly more inefficient than they need to be regardless of the GC.
Nonetheless, for performance C++ code you don’t see much malloc anywhere, directly or indirectly, so the efficiency doesn’t matter that much. That’s pretty idiomatic. I think this is the real point. As a C++ programmer, if you are not serious about eliminating dynamic allocation then you aren’t serious about performance. Since C++ is used for performance, you don’t see much dynamic allocation that a GC could theoretically help with. Most objects in the hot path have explicitly and carefully managed lifetimes for performance.
I use GC languages for quite a few things, but it is always for things where performance doesn’t matter. When performance matters, I’ve always been able to beat a GC for performance, and I’ve done my fair share of performance engineering in GC languages.
If you compile that code with Fil-C++, then you won't see much (or any) GC overhead. The GC kicks in for three things:
- malloc (this is the main client of the GC)
- stack allocations that aren't escape analyzed (these are rare, typically small, and generate minuscule GC load - it's common for code I've tested to have zero of these)
- Fil-C runtime internal allocations (these are super rare - for example if you call sigaction, pthread_create, setjmp, or dlopen then there's some GC object allocated behind the scenes - but you aren't going to be calling those a lot, and if you are then you've got bigger problems than GC).
I understand your perspective about GC languages. Most GC languages also come with other baggage, which makes it very difficult to avoid the GC. Most C++ code that avoids malloc ends up doing a bunch of `new`s when converted to Java. But C++ code that avoids malloc will avoid the GC if compiled with Fil-C++.
Fil-C’s compiler is GC aware.
If I could use C++ with GC, I would - but I can’t because other than Fil-C++ no GC works soundly with the full C++ language, and those that work at all tend to be unreasonably slow and flaky due to conservatism-in-the-heap (Boehm, I’m looking at you). Reason: GC and memory safety are tightly coupled. GC makes memory safety easy, but GC also requires memory safety to be sound and performant.
So there isn’t anything else out there quite like Fil-C++. Only Fil-C++ gives you accurate high perf GC and the full C++ language.
Finally, “affording” GC isn’t really a thing. GC performs well when compared head to head against malloc. It’s a time-memory trade off (GC tends to use more memory but also tends to be faster). If you want to convince yourself, just try a simple test where you allocate objects in a loop. Even JS beats C++ at that benchmark, because GCs support higher allocation rates (just make sure to make both the C++ and the JS version complex enough to not trigger either compiler’s escape analysis, since then you’re not measuring the allocator).
Secondly you have places where you don't have dynamic memory at all which you're also conveniently ignoring.
If your C code doesn’t dynamically allocate then it won’t create any GC load in Fil-C.
what about performance/throughput compared to when you allocate stuff on stack?
It should never have been there in first place, because it ignored their requirements, and thus it was never adopted by them or anyone else.
> On the other hand, Fil-C is quite slow. It's ~10x slower than legacy C right now (ranging from 3x slower in the best case, xz decompress, to 20x in the worst case, CPython).
That performance loss is severe and makes the approaches totally uninteresting for a most serious use cases. Most applications written in C or C++ don't get to waste that many cycles.
Note that at the start of this year it was 200x slower. I land speed ups all the time but don’t always update the readme every time I land an optimization. Perf is the main focus of my work on Fil-C right now.
Right now I’m spending all my time actually implementing optimizations and measuring them locally. I did spend the time to give myself a good benchmark suite with a nice harness (I call it PizBench, it includes workloads from xzutils, simdutf, Python, OpenSSL, and others).
Python does shit like that and it works fine in Fil-C, though I needed to make some minor adjustments a like a 50KB ish patch to CPython.