As of February 1, 2015, new iOS apps uploaded to the Apple App Store must include 64-bit support. Our engine was already “64-bit ready”, but there are always surprises and new challenges when adding support for a new architecture or platform, and iOS 64-bit was no different. While updating Marvel Puzzle Quest for iOS 64-bit, we also managed to make a pass at making the engine more robust and consistent and now our engine is in a better place now to handle memory paradigm shifts in the future. What follows are the highlights of our experience, which may be helpful to others dealing with similar changes.
iOS 64-bit, for both ARM64 (an iOS device) and x86-64 (the iOS Simulator), follows the LP64 programming model. The main differences between the ILP32 model used for ARM7/x86-32 and the LP64 model used for ARM64/x86-64 are the 64-bit pointer type (
void*) and the 64-bit long integer type (
long). These differences are in line with our other 32-bit vs. 64-bit platforms. What was more surprising, is that the Objective-C
CGFloat types also expand to 64-bits. For Marvel Puzzle Quest, this lead to some type wrangling and sanity checking while porting our iOS platform abstraction layer. It also resulted in some long discussions about the appropriate bit width for various types.
As guidelines for choosing the bit width of a type, we broadly bucket types into a few use cases:
Address: an unsigned integer type at the architecture bit width (e.g. 32-bit for iOS 32-bit, 64-bit for iOS 64-bit). Used for absolute memory addresses, large memory buffer sizes, and low-level bit twiddling of pointers. Equivalent to
size_ton most platforms.
Container: a 32-bit unsigned integer used for indexing into and returning the size of container types (e.g. array, linked list, and hash table). This differs from the STL convention of using
size_tfor this purpose.
- File: a 32-bit unsigned integer type for file read sizes, a 64-bit signed integer type for file offsets, and a 64-bit unsigned integer type for absolute file sizes. We chose a 32-bit unsigned integer type for our read APIs since these operations typically occur into containers or container-sized buffers.
Although critical to resolve, bit width changes in our platform abstraction layer tend to be nicely encapsulated and low impact. Historically, bit width changes that affect runtime file formats tend to have the biggest ripple-effects. Fortunately for Marvel Puzzle Quest, our one file format that is bit width dependent is small and quick for our content pipeline to produce. As a result, we chose the simplest solution to support iOS 64-bit: generate a single “fat content file”, which contains both 32-bit and 64-bit data, and update our runtime code to read the appropriate file chunk for the active architecture. This isolated code changes and prevented any widespread impacts of this support on our runtime code and content pipeline.
Beyond bit width changes, the biggest challenge of iOS 64-bit for Marvel Puzzle Quest has been the increase in app size. The addition of an ARM64 chunk to the ARM7 chunk in our app’s fat binary lead to a size increase of approximately 230%! Given how poorly app binaries compress when submitted to the Apple App Store, this had a significant impact on our download size and bumped us up against the 100 MB over-the-air App Store download limit.
For the short term, we squeaked under the download limit by tweaking content compression. In the long term, we’ll need to move more content into our on-demand content delivery (see Just-in-time Content Delivery). We’ll likely also put some time into executable size optimizations by pruning stale code (we already compile with -Os).
Overall, the move to iOS 64-bit has given us an opportunity to suss out bugs and to increase the robustness and flexibility of our engine code. Future work will include a more robust and long term solution to account for our increased binary size with ARM64 and to evaluate the performance impact of the extra memory and cache pressure of 64-bit instructions.