Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Profiling Objective-C binary image size

Tags:

I'm looking for tools and approaches to determining what parts of of my Cocoa and Cocoa-Touch programs are most contributing the the final binary image size, and ways to help reduce it. I'm not looking for a "magic bullet" compiler flag. I'm looking for profiling techniques for evaluating and reducing image size waste in the same vein as Shark and Instruments help for run-time evaluation.

A first-order approximation may be the size of the .o's, but how trustworthy is this in terms of final image size after optimizations and dead-code stripping? If I add up all the .o's they are much larger than my final image, so clearly the linker is already helping me out significantly. But this means the size of the .o's may not be a useful measure.

Where do others look to reduce image size without undermining code maintainability?

like image 341
Rob Napier Avatar asked Jun 06 '09 20:06

Rob Napier


1 Answers

Apple has some awesome docs on Code Size Performance Guidelines, almost all of which applies to this question in some form. There are even tips for pedantic approaches like manually ordering symbols in the binary if desired. :-)

I'm totally a fan of simple, slim code and minimizing disk/memory footprint. Premature optimization is always a bad idea, but consistent housekeeping can be a good way to prevent cruft from accumulating. Unfortunately, I don't know of an automated way to profile code sizes, but several tools exist that can help provide specific insight.

Binary Image Size

Object files aren't as terrible an approximation as you'd guess. One reason the sum is smaller than the parts is because the code is all joined together with a single header. Although the percentages won't be precise, the biggest object files are the biggest parts of the linked binary.

For understanding the raw length of each particular method in an object file, you could use /usr/bin/otool to print out the assembly code, punctuated by Objective-C method names:

$ otool -tV MyClass.o 

I look for long stretches of assembly that correspond to relatively short or simple methods and examine whether the code can be simplified or removed entirely.

In addition to otool, I've found that /usr/bin/size can be quite useful, since it breaks up segments and sections hierarchically and shows you the size of each, both for object files and compiled binaries. For example:

$ size -m -s __TEXT __text MyClass.o $ size -m /Applications/iCal.app/Contents/MacOS/iCal 

This is a "bigger picture" view, although it usually reinforces that __TEXT __text is often one of the largest in the file, and hence a good place to start pruning.

Dead Code Identification

Nobody really wants their binary to be littered with code that is never used. In a dynamic and loosely-coupled language like Objective-C, it can be difficult or impossible to statically determine whether specific code is "used" or not. Even if a class is instantiated or a method is called, tracing code paths (both theoretical and actual) can be a headache. I use a few tricks to help with this.

  • For static analysis, I strongly recommend the Clang Static Analyzer (which is happily built into Xcode 3.2 on Snow Leopard). Among all its other virtues, this tool can trace code paths an identify chunks of code that cannot possibly be executed, and should either be removed or the surrounding code should be fixed so that it can be called.
  • For dynamic analysis, I use gcov (with unit testing) to identify which code is actually executed. Coverage reports (read with something like CoverStory) reveal un-executed code, which — coupled with manual examination and testing — can help identify code that may be dead. You do have to tweak some setting and run gcov manually on your binaries. I used this blog post to get started.

In practice, it's uncommon for dead code to be a large enough proportion of the code to make a substantial difference in binary size or load time, but dead code certainly complicates maintenance, and it's best to get rid of it if you can.

Symbol Visibility

Reducing symbol visibility may seem like a strange recommendation, but it makes things much easier for dyld (the linker that loads programs at runtime) and enables the compiler to perform better optimizations. Consider hiding global variables (that aren't declared as static) etc. by prefixing them with a "hidden" attribute, or enabling "Symbols Hidden by Default" in Xcode and explicitly making symbols visible. I use the following macros:

#define HIDDEN __attribute__((visibility("hidden"))) #define VISIBLE __attribute__((visibility("default"))) 

I find /usr/bin/nm invaluable for identifying unnecessarily visible symbols, and for identifying potential external dependencies you might be unaware of or hadn't considered.

$ nm -m -s __TEXT __text MyClass.o  # -s displays only a given section $ nm -m -p MyClass.o  # -p preserves symbol table ordering (no sort)  $ nm -m -u MyClass.o  # -u displays only undefined symbols 

Although reducing symbol visibility is unlikely to directly reduce the size of your binary, the compiler may be able to make improvements it couldn't otherwise. Also, you stand to reduce accidental dependencies on symbols you didn't intend to expose.

Analyzing Library Dependencies and Loading

In addition to raw binary size, it can often be quite helpful to analyze which dynamic libraries you link to, and eliminate those that might be unnecessary, particularly less-commonly-used frameworks that may not be loaded yet. (You can also see this from Xcode too, but with complex projects, sometimes things slip through, so this also makes for a handy sanity check after building.) Again, otool to the rescue...

$ otool -L MyClass.o 

Another (extremely verbose) alternative is to have dyld print loaded libraries, like so (from Terminal):

$ export DYLD_PRINT_LIBRARIES=1 $ /Applications/iCal.app/Contents/MacOS/iCal 

This shows exactly what is being loaded, including dependencies of the libraries your code links against.

Analyzing Launch Performance

Usually, what you really care about is whether the code size and library dependencies are truly affecting launch time. Setting this environment variable will cause dyld to report load statistics, which can really help pinpoint how time was spent on load:

$ export DYLD_PRINT_STATISTICS=1 $ /Applications/iCal.app/Contents/MacOS/iCal 

On Leopard and later, you'll notice entries about "dyld shared cache". Basically, the dynamic linker creates a consolidated "super library" composed of the most frequently-used dynamic libraries. It is mentioned in this Apple documentation, and the behavior can be altered with the DYLD_SHARED_REGION and DYLD_NO_FIX_PREBINDING environment variables, similar to above. See man dyld for details.

like image 151
11 revs Avatar answered Oct 21 '22 18:10

11 revs