So ... About power consumption and ARC, would it be a stretch to say that because of how ARC is designed it is more efficient at power output than GC? Or is it more complex than that?
By the way, thank you all for these answers, they are really helpful for me to understand the diff now.
If you remember nothing else from this blog post,remember this chart. The Y axis is time spent collecting garbage. The X axis is ârelative memory footprintâ. Relative to what? Relative to the minimum amount of memory required.
What this chart says is âAs long as you have about 6 times as much memory as you really need, youâre fine. But woe betide you if you have less than 4x the required memory.â But donât take my word for it:
This thread has had a lot of emphasis on the benefits of reference counting, namely:
Lower high-memory-watermark, because memory is continuously cleared on-the-fly, not leaving any "garbage" to pile up until the next gc run.
Deterministic(ish) destruction of objects, which makes deinit feasible to use for resource management (as opposed to object destructors/finalizers, which are discouraged in almost every GCed environment).
RC scales better with high object counts, which would otherwise make GCs take longer per pause.
I'd like to balance the conversation by mentioning the main downsides:
The need for devs to manually break reference cycles with weak references
Retain/release calls limit throughput (particularly atomic ones), and add up to a substantial portion of time spent in a typical Swift program.
GCed programs can have higher throughput, at the cost of the GC pauses. In some cases, that can be mitigated if they can be cleverly scheduled e.g. between requests on a server (so no one is waiting), or breaking it up and squeezing it into idle time between frames.
GC has already had enough time (multiple decades) to prove that it can be efficient enough, but afaict it never really delivered any significant improvements. A technology that is based on periodically traversing the entire object tree, at best can cut some branches but will probably always do some redundant work anyway, as well as will always require extra RAM.
It just doesn't seem like a great technology although it does reduce the burden of memory management on the developers (ARC makes you watch out for circular references) so to many it feels like a great compromise. But in the age of mobile devices and laptops it increasingly becomes a problem developers can't and shouldn't ignore anymore.
Think misconception is to equal GC to JVM GC, which is not always true. What'd you say about Haskell or Erlang GC?
TheOtherMobileOS literally runs fine on JVM. I hate it, but it works.
Tbh my points were mostly for languages overall, including distributed systems and servers (which author actually asks about), not only mobile devices and laptops.
Java garbage collectors are more performant than those in either Haskell or Erlang.
This is clearly false with a generational garbage collector.
Reference counting vs GC involves a complex set of tradeoffs but itâs not true that one is simply better than the other, and we shouldnât make false claims when advocating either approach.
Yes, but bet Haskell runtime + it's GC is more/same power efficient as Java (could be completely off though need to double check, at least should be memory efficient). And Erlang would be last language to measure performance and power efficiency, I guess. But if you move Erlang VM to something like JVM plus it's GCâyou'll loose all the benefits of concurrency. Every language has its purpose and idea behind.
And that's my pointâyou cannot measure GC as an idea by just one factor.
GC will use more memory mostly, so not sure electricity bill would be 30-50 higher. And from my very little experience it's non obvious when comparing results on one device vs. well built distributed system.
It was based on the benchmark I mentioned earlier in this thread, that Android requires 30% more battery capacity for the same performance and battery life compared to iOS. The hypothesis was that it's the GC's fault.
itâs worth noting that many who have tried using deinit semantics for resource management have found it counterproductive in the end, and iâve seen some movement (in SwiftNIO, NIOHTTP2, etc.) away from that pattern and towards explicit lifetime management.
iâve said this before but iâll link it since itâs relevant:
electricity is expensive, but itâs nowhere near as expensive as paying human beings to spend time engineering ways to shave a small percentage off an electricity bill.
I know this is the popular answer, but I really don't find it compelling (and I disagree with some of my teammates on this, so this is just my personal stance!). Here's my reasoning:
I presume that using an object after invalidating it is incorrect, because necessary state will have been torn down in invalidate, if this is not the case then we may be talking about different things
This means that use-after-invalidate is now a new class of bug, similar to use-after-free
Preventing use-after-free bugs by being careful about where you call free is the status quo for C that we invented referencing counting and garbage collection to fix
And indeed, in large systems that I won't name that use the "invalidate" pattern, I've seen them painstakingly reinvent NSZombieEnabled and friends, clawing their way back to the status quo one uaf-debugging feature at a time.
Meanwhile, I've seen other systems go exactly the opposite direction: os_transaction initially had paired _begin and _end functions, but chasing down bugs caused by incorrect pairing proved very difficult. The newer API for it is an object specifically so that standard heap inspection tools like leaks and Instruments can be used to solve transaction leaks.
well, the problem with cleanup on deinit is that oftentimes, the cleanup is failable. and sometimes it is not only failable, but async as well.
now you canât really fail-out of a deinit (and you canât do anything async at all), so what ends up happening in practice is that these cleanup failures turn into fatal errors. and you should never ever crash on the server side if you can help it. so these deinit APIs have no choice but to evolve into closure APIs.
The paper is a bit too long for me, sorry, but I think I get the point from the introduction.
Fundamentally both ARC and GC perform some or a lot of unnecessary work: the GC can repeatedly traverse objects that don't need to be freed while ARC can increment and decrement counters that are greater than 1. Both can be wasteful, however ARC clearly wins in terms of memory footprint, consequently cache locality, let alone determinism which is not exactly a measurable quality.
It's just beyond me why a language designer would choose GC for their new language in the 21st century. No, GC's are not improving over time in any significant way, and no, ARC's potential memory leaks do not overweigh GC's horrific waste of RAM: at least one is fixable and the other one is not.
Anyway, I'm obviously in the ARC camp and I love Swift for that
Xojo (previously REALBasic) has ARC, and I believe has since they first moved away from being a Java front end in the 90s to being their own full compiler.
tbh Swift and ObjC's ARC are both relatively unusual compared to traditional refcounting languages:
Many older refcounting implementations, including older versions of ObjC, used locks and external storage rather than inline atomics
Recent CPUs have dramatically faster atomics
Swift's ARC optimizer is the product of a truly incredible amount of effort by the compiler team, which is not an appealing prospect to most teams creating a new language (and is infeasible for languages that aren't using an optimizing compiler, like interpreters and first-stage JITs)
Many recent languages are designed with assumption that there won't be 400+ processes all loading a copy of the runtime simultaneously, running on a small mobile device
Swift's non-CoW value types are not refcounted at all, and its CoW value types trade refcounting overhead for avoiding copies
Similarly, ObjC does not refcount primitives, and high performance code is expected to use raw C pointers in many situations (e.g. -[NSArray getObjects:count:], or the hidden C buffer used by for( in ))
Given all that it's not surprising at all to me that many language designers made different choices.