In an iOS project I developed some months ago, I needed to create lots of strings in one of my algorithms. And by lots of I mean some millions. Those strings were actually alive for a very short time, but just the fact of creating them was too much time-consuming, so I needed a way to improve that step.

The original way I was creating all those strings was with NSString and one of its standard class methods, as follows:

[NSString stringWithFormat:@"My number is %i", myNumber]

That works perfectly fine most of the time. But it can become a bottleneck when creating and destroying a huge number of strings. At this point I realized that, as long as Objective-C is a strict superset of C I could use strings in the standard way in C, that is using char[], as shown here:

#define STRING_SIZE 22
char c[STRING_SIZE];
sprintf(c, "My number is %i", i);

The execution was certainly faster with this last approach. But faster is not accurate enough, so I created a little Objective-C project to test both options and compare how fast (or slow) they were. In this project there are two classes: AMGStringInObjectiveC and AMGStringInC, both containing only a property/variable of type NSString or char[].

This is the code for the first (using NSString):

// AMGStringInObjectiveC.h

#import <Foundation/Foundation.h>

@interface AMGStringInObjectiveC : NSObject

- (id)initWithString:(NSString *)string;

@end

// AMGStringInObjectiveC.m

#import "AMGStringInObjectiveC.h"

@interface AMGStringInObjectiveC() 
@property (nonatomic, copy) NSString *myString; 
@end

@implementation AMGStringInObjectiveC

- (id)init
{
    return [self initWithString:nil];
}

- (id)initWithString:(NSString *)string
{
    self = [super init];
    if (self) {
        _myString = string;
    }
    return self;
}
@end

And this is the code for the second (using char[]):

// AMGStringInC.h

#import <Foundation/Foundation.h>

#define STRING_SIZE 22

@interface AMGStringInC : NSObject

- (id)initWithString:(char *)string; 

@end

// AMGStringInC.m

#import "AMGStringInC.h"

@interface AMGStringInC()
{
    char myString[STRING_SIZE];
}
@end

@implementation AMGStringInC

- (id)init
{
    return [self initWithString:NULL]; 
}

- (id)initWithString:(char *)string 
{
    self = [super init];
    if (self) {
        strcpy(myString, string);
    }
    return self;
} 
 
@end

And then I created a battery of tests, checking execution time for 100,000, 500,000, 1,000,000, 5,000,000 and 20,000,000 strings and running each test five times to get the arithmetic mean and the standard deviation. This was the code used:

// For the NSString option
NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < NUM_STRINGS; i++) {
    [array addObject:[[AMGStringInObjectiveC alloc] initWithString:[NSString stringWithFormat:@"My number is %i", i]]]; 
} 

// For the char[] option
NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < NUM_STRINGS; i++) {
    char c[STRING_SIZE];
    sprintf(c, "My number is %i", i);
    [array addObject:[[AMGStringInC alloc] initWithString:c]];
}

Of course this isn’t a valid method to check execution times. We would need much deeper tests, but it’s enough to get an idea. These were the results in milliseconds for the NSString option:

Elapsed time for 100000: 0.3437 
Elapsed time for 100000: 0.3448 
Elapsed time for 100000: 0.3404 
Elapsed time for 100000: 0.3602 
Elapsed time for 100000: 0.3424 
Mean execution time = 0.346 +/- 0.007

Elapsed time for 500000: 1.6950 
Elapsed time for 500000: 1.7129 
Elapsed time for 500000: 1.6920 
Elapsed time for 500000: 1.7163 
Elapsed time for 500000: 1.7265 
Mean execution time = 1.709 +/- 0.013

Elapsed time for 1000000: 3.4524 
Elapsed time for 1000000: 3.4038 
Elapsed time for 1000000: 3.3992 
Elapsed time for 1000000: 3.3813 
Elapsed time for 1000000: 3.4293 
Mean execution time = 3.413 +/- 0.025

Elapsed time for 5000000: 16.9836 
Elapsed time for 5000000: 16.9589 
Elapsed time for 5000000: 17.2482 
Elapsed time for 5000000: 17.2215 
Elapsed time for 5000000: 17.2685 
Mean execution time = 17.136 +/- 0.136

Elapsed time for 20000000: 68.1141 
Elapsed time for 20000000: 68.1765 
Elapsed time for 20000000: 67.9571 
Elapsed time for 20000000: 68.4114
Elapsed time for 20000000: 68.6366 
Mean execution time = 68.259 +/- 0.239

And these are the results for the char[] option:

Elapsed time for 100000: 0.1110 
Elapsed time for 100000: 0.0993 
Elapsed time for 100000: 0.0973 
Elapsed time for 100000: 0.1076 
Elapsed time for 100000: 0.0928 
Mean execution time = 0.102 +/- 0.007

Elapsed time for 500000: 0.4479 
Elapsed time for 500000: 0.6306 
Elapsed time for 500000: 0.5551 
Elapsed time for 500000: 0.6215 
Elapsed time for 500000: 0.6227 
Mean execution time = 0.576 +/- 0.069

Elapsed time for 1000000: 0.9002 
Elapsed time for 1000000: 1.1771 
Elapsed time for 1000000: 1.1597 
Elapsed time for 1000000: 1.1182 
Elapsed time for 1000000: 1.0287 
Mean execution time = 1.077 +/- 0.102

Elapsed time for 5000000: 4.5200 
Elapsed time for 5000000: 4.8681 
Elapsed time for 5000000: 4.7976 
Elapsed time for 5000000: 4.7932 
Elapsed time for 5000000: 4.7769 
Mean execution time = 4.751 +/- 0.120

Elapsed time for 20000000: 18.0116 
Elapsed time for 20000000: 18.7309 
Elapsed time for 20000000: 18.5369 
Elapsed time for 20000000: 18.2186 
Elapsed time for 20000000: 18.3700 
Mean execution time = 18.374 +/- 0.249

For 20 million strings the NSString approach takes 68 seconds for just 18 using char[] instead. Similar performance ratios with the other tests.

In fact using char[] strings turns out to be between a 66% and a 73% faster than using the standard NSString. Nonetheless I’m not saying it’s a good idea to use char[] in general. Absolutely not. NSString is the right choice in the vast majority of cases. But it’s good to bear in mind that at the end of the day Objective-C is a superset of C and so any code that works efficiently in C will also work in Objective-C and that can be used to our advantage.