Swift, ARC and memory footprint vs JVM and GC

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.

1 Like

Not the exact stat/source, but related: Why mobile web apps are slow | Sealed Abstract

image
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:

4 Likes

This thread has had a lot of emphasis on the benefits of reference counting, namely:

  1. Lower high-memory-watermark, because memory is continuously cleared on-the-fly, not leaving any "garbage" to pile up until the next gc run.
  2. 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).
  3. 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:

  1. The need for devs to manually break reference cycles with weak references

  2. 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.

9 Likes

In general yes, power efficiency is one of the reasons, I suppose, that Swift has ARC, and not GC. From old (pre-forum) threads:

(emphasis mine)

3 Likes

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.

2 Likes

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. :sweat_smile:
Tbh my points were mostly for languages overall, including distributed systems and servers (which author actually asks about), not only mobile devices and laptops.

Imagine this: you save 30-50% on your data center electricity bill if you don't use GC...

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.

14 Likes

Yes, but bet Haskell runtime + it's GC is more/same power efficient as Java (could be completely off though :upside_down_face: 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.

Some fun classic papers for this discussion

"Hippocratic" garbage collection, that cooperates with the kernel VM subsystem: https://sfkaplan.people.amherst.edu/courses/2004/spring/cs40/papers/HFB:PLCGC.pdf

An interesting claim that tracing garbage collectors and reference counting are more closely related than they appear: https://courses.cs.washington.edu/courses/cse590p/05au/p50-bacon.pdf

3 Likes

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.

2 Likes

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.

6 Likes

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.

1 Like

Indeed. My point wasn't "RAII-style resource management has no issues", it was more "there is no truly good answer here currently"

1 Like

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 :innocent:

I always wondered about this. If ARC is so great why only Swift and Objective-C/++ has it. AFAIK no other language has ARC.

Versions of ARC exist in a lot of languages. Offhand, the Nim language, which started with a GC, added a version of ARC in its recent v2 release.

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.

1 Like

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.

9 Likes