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