blog

Photot of memory by Photos Hobby on Unsplash

Radically Cross Platform: Memory Management

by

The first post of this series described a choice of technologies and toolsets (based on Xamarin) that allows C# programmers to deploy graphics accelerated apps using OpenGL/DirectX across a radically cross platform spectrum of devices and operating systems.

That’s all fine and good, but it’s not the whole story.  Just because you can build and run your app on a given platform doesn’t mean it’s ready to publish on an app store.  Well, in the case of Windows and Mac x86 desktop apps, maybe it does.  But when you take a code base with beautiful LINQ queries, effortless, strictly-typed data modeling, and world-class garbage collector (GC) expectations – and run them on a wimpy ARM-based mobile device, a funny thing happens.

It turns out that memory capacity and CPU power are not unlimited resources on a mobile device.  Who knew, right?

Excessive Memory Use

Probably the most basic limitation will be that you can’t allocate huge chunks of memory like you can on the desktop.  My primary desktop machine has 32 GB of RAM, which is about 500,000 times the memory capacity of my first computer.  When we go mobile, we have to back away from our reliance on modern desktop-class computing capacity.  Some of my target devices have only 256 MB of RAM — a number that includes the operating system and any other resident apps.  (And 1986-me feels "so sorry" for 2013-me, having in this case only 4,000 times the RAM to play with.)

A hard memory limit is a affliction both of managed and unmanaged apps, and both simply have to be careful both "where" and "how much" they allocate, to make sure they stay within limits for the platform.

So what happens if you run out of memory?  On iOS, the operating system tries to give a friendly early notification to apps that they should take emergency measures to release inactive resources.  Most apps ignore those warnings, of course.  The next step is crashing.  If a mobile OS runs out of memory, it will forcibly terminate the current process.  That is the correct answer from the OS’s point of view, but it doesn’t make us as the app developer look very good.  If you use mobile apps very much, you’re probably aware that even grade A apps from top tier publishers are susceptible to this; it’s kind of a hard problem to solve.  Fortunately, as platforms advance and silicon dies shrink, the boundaries are moving in a favorable direction.

GC Churn

This is closely related to the problem of excessive memory use, and can in fact trigger the same problem.  A mobile GC tries to release memory in time to avoid crashing, but it’s a delicate balance that doesn’t always work — don’t rely on the GC to save you.

Even if you don’t crash, it’s still a problem.  In a smoothly rendered, graphics accelerated app, the GC disrupts your user experience.  When the GC periodically collects, all threads on your app get their execution paused to avoid corruption.  For your end user, this will show up as a confusing and ugly delay.

Peter Banning picks up a knife and fork at the dinner table.  All the Lost Boys stare at him.
Peter: “What?”
Lost Boy: “We don’t use ’em.”
Peter: “If you don’t use them–then why are they here?”
Lost Boy: “So we don’d have to use ’em.”

— From Hook! Screenplay by Jim Hart, June 1990

I love that C# is a managed language, because that’s great for static binding and rigorous error checking, but for graphical apps on mobile, the goal is to never run garbage collection during the course of a game level or a edutainment activity.  "We have it so that we don’t have to use it."  So how can we do that?  By managing memory ourselves.  There are two main strategies for this, that are in general both necessary:

Pools

It sounds less scary to say you’re implementing a pool rather than saying you’re writing your own memory manager.  And fortunately, it is just about as easy as it sounds.  C#’s generics make it a piece of cake.  I’ve added some extras to mine, such as:

  • Special default handling of types that are themselves a List<T>, for example it’s assumed that the list should be cleared when disposed.
  • Optional delegate parameter to the pool constructor to (re)initialize special types.
  • Choice of either manual or autorelease logic — i.e. autorelease points established strictly based on call stack with no multithreading provisions.

Structs versus Classes

One of the best ways to avoid memory management costs is to allocate memory on the stack instead of the heap.  With the heap, managing memory involves all kinds of pointer based lists behind the scenes, and repeated iteration of those complex data structures by the GC.  With the stack, your app’s compiled code is just inc/decrementing a simple integer for each thread as it pushes and pops along its merry way through your function invocations.

To allocate memory on the stack, declare your user defined data types as struct instead of class.  This will change how you may use the objects, and there are both benefits and drawbacks.

The big drawback is that you can no longer rely on the convenience of pointers to ensure non-duplication of data.

The benefits include:

  • Allocation and use of a struct can be very fast.  In some inner loop optimization situations, you can even avoid calling a constructor.
  • You can realize some of the benefits of immutability in multithreaded usage, since you’re always passing around copies of the objects instead of pointers to them.
  • As mentioned, allocation is all on the stack, which avoids garbage generation.  So no cleanup required.

Strings

Strings deserve their own discussion here, because if handled poorly, they can be a source of garbage generation.  Take for example a format function call, that expands something of the form "Value: {0}" into "Value: MyValue".  In C# as in many other languages, the app will generally allocate memory on the heap for this string, resulting in extra GC work.

In creating my graphical app/game engine I dealt with this issue in two ways: aggressive caching where appropriate; and use of the StringBuilder as a poolable and mutable alternative to String.  In my embedded scripting language, the string variable type maps directly to StringBuilder, and my runtime interpreter uses pooled allocation and auto-releasing semantics when evaluating string expressions.  Now, this wasn’t particularly fun to debug (e.g. support for closures), but once it works, it works, and the whole app engine is better for it.

Summary

In this post I described some of the challenges of taking a C# code base and making it ready for deployment as a mobile app — it requires changing the way you think about memory consumption, and taking over some of the job of the garbage collector (GC) that we’re used to relying on with desktop development.  With careful use of these techniques, we can enjoy some important benefits of a managed language such as strict static checking, while still keeping our app’s execution lean and fast.

+ more

Accurate Timing

Accurate Timing

In many tasks we need to do something at given intervals of time. The most obvious ways may not give you the best results. Time? Meh. The most basic tasks that don't have what you might call CPU-scale time requirements can be handled with the usual language and...

read more