Recently I started working on a project that used NSLog extensively. When building for the device/release, stderr was redirected to a file so that everything logged through NSLog was written to a file in the app’s Documents directory.
Preliminary Analysis
I was asked to look into some performance problems, and the first thing that I noticed was that a significant amount of time was spent spinning on malloc’s OSSpinLock. So then I looked at the heap allocation history and noticed that, in this app, there were hundreds of thousands of small, short-lived allocations attributed to NSLog during this time period. Further sampling showed that a significant amount of CPU time was being spent in functions with NSLog somewhere on the callstack.
Quick Check
Given this information, the first order of business was to prove that NSLog was indeed the root cause of the performance problems. So I decided to turn every NSLog call into a nop. This is pretty easy to do if you have a single header file that each source file in your project includes. In my case, there was no such explicit shared header, but in a situation like this where you are just making a quick hack, you can use the project’s precompiled header. So I temporarily inserted the following line into the pch file:
#define NSLog(fmt, ...) ((void)0)
Next I rebuilt the project and did my performance assessment. I observed that performance had markedly improved–I had found the culprit.
More Analysis
So why was NSLog causing such performance degradation and why was it creating so many memory allocations? Doesn’t it just do string formatting and write the output to stderr? Let’s see what’s happening here:
First, let’s set some Bash variables to make this easier to read and type:
$ OTOOL=/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/otool $ FOUNDATION=/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS3.0.sdk/System/Library/Frameworks/Foundation.framework/Foundation
Now let’s see how NSLog is implemented:
bogrov:~ $ $OTOOL -toV -arch armv6 $FOUNDATION | grep -B0 -C24 'NSLog:' _NSLog: 305666d4 b40f push {r0, r1, r2, r3} 305666d6 b580 push {r7, lr} 305666d8 af00 add r7, sp, #0 305666da b081 sub sp, #4 305666dc ab03 add r3, sp, #12 305666de a904 add r1, sp, #16 305666e0 cb04 ldmia r3!, {r2} 305666e2 1c10 adds r0, r2, #0 305666e4 9203 str r2, [sp, #12] 305666e6 9300 str r3, [sp, #0] 305666e8 f000f805 bl _NSLogv 305666ec b001 add sp, #4 305666ee bc80 pop {r7} 305666f0 bc08 pop {r3} 305666f2 b004 add sp, #16 305666f4 4718 bx r3 _NSLogv: 305666f6 b5f0 push {r4, r5, r6, r7, lr} 305666f8 af03 add r7, sp, #12 305666fa b082 sub sp, #8 305666fc 1c04 adds r4, r0, #0 305666fe 2000 movs r0, #0 30566700 1c0d adds r5, r1, #0 30566702 f7deff2d bl _NSPushAutoreleasePool
bogrov:~ $ $OTOOL -toV -arch armv6 $FOUNDATION | grep -B0 -C48 'NSLogv:' _NSLogv: 305666f6 b5f0 push {r4, r5, r6, r7, lr} 305666f8 af03 add r7, sp, #12 305666fa b082 sub sp, #8 305666fc 1c04 adds r4, r0, #0 305666fe 2000 movs r0, #0 30566700 1c0d adds r5, r1, #0 30566702 f7deff2d bl _NSPushAutoreleasePool 30566706 4b12 ldr r3, [pc, #72] (0x30566750) 30566708 447b add r3, pc 3056670a 681b ldr r3, [r3, #0] 3056670c 1c06 adds r6, r0, #0 3056670e 2b00 cmp r3, #0 30566710 d00f beq.n 0x30566732 30566712 4910 ldr r1, [pc, #64] (0x30566754) 30566714 4810 ldr r0, [pc, #64] (0x30566758) 30566716 4479 add r1, pc 30566718 2200 movs r2, #0 3056671a 6809 ldr r1, [r1, #0] 3056671c 2304 movs r3, #4 3056671e 4478 add r0, pc 30566720 9400 str r4, [sp, #0] 30566722 9501 str r5, [sp, #4] 30566724 f064eef4 blx 0x305cb510 ; symbol stub for: __CFLogvEx 30566728 1c30 adds r0, r6, #0 3056672a f7e1fc0e bl _NSPopAutoreleasePool 3056672e b002 add sp, #8 30566730 bdf0 pop {r4, r5, r6, r7, pc} 30566732 490a ldr r1, [pc, #40] (0x3056675c) 30566734 2200 movs r2, #0 30566736 4479 add r1, pc 30566738 2304 movs r3, #4 3056673a 6809 ldr r1, [r1, #0] 3056673c 2000 movs r0, #0 3056673e 9400 str r4, [sp, #0] 30566740 9501 str r5, [sp, #4] 30566742 f064eee6 blx 0x305cb510 ; symbol stub for: __CFLogvEx 30566746 1c30 adds r0, r6, #0 30566748 f7e1fbff bl _NSPopAutoreleasePool 3056674c b002 add sp, #8 3056674e bdf0 pop {r4, r5, r6, r7, pc} 30566750 321c adds r2, #28 30566752 07fc lsls r4, r7, #31 30566754 bb12 cbnz r2, 0x3056679c 30566756 07f9 lsls r1, r7, #31 30566758 ce9b ldmia r6!, {r0, r1, r3, r4, r7} 3056675a 0002 lsls r2, r0, #0 3056675c baf2 revsh r2, r6 3056675e 07f9 lsls r1, r7, #31
Regardless of whether it takes the branch at 0×30566710, it eventually calls _CFLogvEx. Fortunately we don’t have to disassemble _CFLogvEx because CoreFoundation is open source. So the quickest way to find the implementation of _CFLogvEx is to Google it: “_CFLogvEx site:opensource.apple.com”. This gives a result for CFUtilities.c
Now we can look at _CFLogvEx’s implementation in C rather than disassembled ARM machine code. Notice that _CFLogvEx doesn’t do much except ask CFString to create the formatted string and then calls __CFLogCString, which is defined in the same file.