6 ms·
Speaking of GC, why aren't more newer languages designed using the ARC system that Objective-C uses? It seems to work amazingly well, it's very high performant
by penpapersw 9y ago
Speaking of GC, why aren't more newer languages designed using the ARC system that Objective-C uses? It seems to work amazingly well, it's very high performant from what I hear, and although it has a few small caveats, they seem worth the trade-off.
- pcwalton 9y agoBecause atomic reference counting is slower than tracing GC. (Yes, really. Adding atomic operations to every load and store of a pointer is a tremendous throughput loss. Latency is not everything!)
- deleted 9y ago[deleted]
- deleted 9y ago[deleted]
- scythe 9y agoLatency might not be everything, but, if you're willing to sacrifice throughput for latency - particularly if you can't get good latency any other way - it's nice to have the option. But I think a deeper reason might be that making reference counting reasonably fast depends on the compiler proving that certain objects aren't shared across threads. If RC itself were slow, nobody would use talloc().
- pcwalton 9y agoIf you want to sacrifice throughput for latency (and you probably don't), use a collector designed for it, like Go's collector or Azul C4. Pervasive thread safe RC sacrifices more throughput than those collectors while simultaneously not actually guaranteeing anything about latency. It's really the worst of all worlds except for implementation simplicity.
- jbooth 9y agoSo what's up with Rust then? Sorry for noob questions but is it: 1) Ownership model means RC only needs to be used rarely in Rust, and ARC even less, minimizing the throughput impact? 2) Simplicity benefits were overwhelming considering the targets you were aiming for? Or something else?
- continuational 9y agoRust adds a lot of cognitive overhead for the ownership and borrowing model. Most languages are not designed like that, because productivity takes precedence.
- unscaled 9y agoMuch of the world is still using C and C++, where every piece of memory has ownership and lifetime. Yes, they don't enforce you to properly specify and transfer ownership semantics at compile time with the borrow checker (although you can enforce some of these things with modern C++), but the cognitive overhead is there. And if you make a mistake you don't get a scary compiler error - you just get a hard to find memory leak, an even harder to find data race or a gaping security hole. Even Go (which usually touted as the perfect alternative to Rust in these conversation), has a lot of cognitive overhead related to ownership. Every time you take a byte slice and pass that forward to another function, you have to know whether that function appends to that slice, in which case your original slice will get overwritten. Case in point: arr := []byte("Hello World") b1 := arr[:6] b2 := append(b1, 'Z') fmt.Println(string(b2)) fmt.Println(string(arr)) https://play.golang.org/p/QIN5SAIL5M https://play.golang.org/p/QIN5SAIL5M Many byte manipulation functions in the go std library and ecosystem have append semantics, for instance: https://godoc.org/regexp#Regexp.Expand https://godoc.org/regexp#Regexp.Expand Better yet, Go often makes it very hard to notice whether you're copying values or just assigning a reference. On the other hand, some languages like Java don't even let you copy values reliably without special support in the class and in the same time highly encourage doing everything with stateful mutable objects. You can't just go and pass all these neat POJOs and beans around to functions modifying their state if they get used around somewhere else, right? This is exactly what all these rust borrow checker warning are all about! The only language I can think of which is completely devoid of ownership semantics overhead is Haskell. Everything is immutable, look ma no side effects! So if this is really the only benchmark for productivity, Haskell is the most productive language ever.
- DougBTX 9y agoARC stands for automatic reference counting, not atomic reference counting, so this criticism doesn't really apply. The LLVM docs say: > ARC makes no guarantees in the presence of race conditions. So, it seems they agree with you, to the extent that they agree that full atomic reference counting would be a bad idea for performance optimisation, which is why they don't do it. https://clang.llvm.org/docs/AutomaticReferenceCounting.html https://clang.llvm.org/docs/AutomaticReferenceCounting.html
- pcwalton 9y agoIt's atomic. The Core Foundation semantics require it. Disassemble Objective-C code if you don't believe me. The race conditions that quote is referring to other kinds of races, not races on the reference count itself.
- xenadu02 9y agoWhat makes you think GC doesn't need atomic operations? Most need write barriers, inject safe points into loops, and other such things that certainly aren't free.
- pcwalton 9y agoWrite barriers don't happen on every read, while reference count manipulation does.
- vvanders 9y agoYeah, reference counting is reasonably fast(and FWIW C++/Rust have good support for it as well) however the one caveat(no cycle tracking) is pretty huge. If you don't have really good tools they're an absolute bear to track down. Personally my go-to solution where I need the flexibility of GC is Lua. It's light-weight, stupid fast for what it is(thanks to LuaJIT). You can also constrain it really easy and running multiple instances is very well supported. If you partition your problem space right you can keep each domain small and thus your root set doesn't grow like it does in JS/UnrealScript/etc. There's actually a lot of parallels with Erlang's root-per-process GC and it. Oh and it's also stupid-easy to drop into any C codebase.
- pjmlp 9y agoObjective-C got ARC after Apple failed to make a tracing GC work without issues when mixing frameworks of multiple origins, specially given the underlying C semantics. So given Objective-C's semantics, having the compiler insert [... retain] and [... release] calls according to Cocoa patterns, makes more sense than the original GC idea. Which only works for Objective-C classes that follow such patterns, not for plain structs or basic C like data types. Swift also adopted ARC, because it needed anyway to interoperate with the Objective-C runtime. RC is nothing new, it was the very first GC algorithm being implemented, there are many reasons why GC research moved beyond it. Making RC algorithms achieve the same kind of performance in multi-core core using NUMA architectures (multi-level caches) is as complex as just having tracing GC algorithms, with the caveat of not handling cycles and possible stack overflows due to cascade deletions.
- coldtea 9y ago>RC is nothing new, it was the very first GC algorithm being implemented, there are many reasons why GC research moved beyond it. It's not about RC, it's about the A in ARC.
- deleted 9y ago[deleted]
- int_19h 9y agoYou can replace "RC" with "ARC" in the comment above, and it will be just as valid. Python has used automatic reference counting for ages, for example.
- kibwen 9y agoAFAIK the secret sauce of ARC is supposed to be the ability to elide retain/release calls when the compiler can prove that they're not needed, which sounds analogous to the escape analysis used in many garbage collectors.
- Joky 9y agoThere is a major difference: 1) it is static and 2) it provides totally stable and reproducible performance behavior.
- naasking 9y ago> Speaking of GC, why aren't more newer languages designed using the ARC system that Objective-C uses Because ARC is just shitty GC.
- dep_b 9y agoIt's not fast at all but it's reliably slow. There's no stop the world event but you'll lose a lot of cycles at any given moment instead.
- geofft 9y agoAs background for discussions of RC vs. GC, keep in mind Bacon et al. 2004's result that refcounting and (tracing) garbage collection are duals of each other: https://www.cs.virginia.edu/~cs415/reading/bacon-garbage.pdf https://www.cs.virginia.edu/~cs415/reading/bacon-garbage.pdf That is, for each RC system of a certain complexity / feature set / etc., there exists a corresponding GC system that requires the same amount of work, and as the RC system does more work, so does the GC system. The exact times at which you have GC pauses, or pool cleanups, or whatever you want to call them are different, but the total amount of overhead is the same. Of course, it's still worth thinking in one of the two paradigms: for instance, if you need precise timings of a function's execution (but it's okay for that function to be slow), you may prefer the immediate overhead of refcounting instead of amortizing the cost into some later garbage collection. But if you can optimize your refcounting overhead based on some application-specific knowledge, loosely speaking, you could also optimize a corresponding GC system by using the same insights.