2

I have written a very simple test application to try to help with a larger project I'm working on.

Simply put, the test app loops a predetermined number of times and appends "1" to a string on each loop. When the loop hits a multiple of 1000, the string is reset and the process starts over again.

The code is below; but what I am finding is that the memory usage is much higher than I would expect. Each iteration adds about .5MB.

It appears that the newString is not reused, but is discarded and a new instance created, without recovering the memory it was using.

Ultimately, the software needs to count much much higher than 100000. As a test, if I change the iteration to 10million, it takes over 5GB memory!

Has anybody got any suggestions? So far I have various ways of writing the clearing of the string and turning off ARC and releasing it/recreating manually, but none seem to be reclaiming the amount of memory I would expect.

Thank you for any help!

*ps. Yes this actual software is totally pointless, but as I say, its a test app that will be migrated into a useful piece of code once fixed.


int targetCount = 100000;
NSString * newString;

int main(int argc, const char * argv[]) {
   @autoreleasepool {
      process();
      return 0;
    }
}

void process() {
    for (int i=0; i<targetCount; i++) {
        calledFunction(i);
    }
}

void calledFunction(count) {
    if ((count % 1000) == 0) {
        newString = nil;
        newString = @"";
    } else {
        newString = [NSString stringWithFormat:@"%@1", newString];
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 2
    What happens if you use more `@autoreleasepool` scopes deeper down in the code (i.e. within the `for` loop of `process()`)? – trojanfoe May 10 '16 at 14:31
  • 1
    Seriously? THAT fixed it?! Lol. trojanfoe you are my new hero! – user2729972 May 10 '16 at 14:32
  • Yeah it's just a question of how frequently the auto-release pools are drained. However this is not a great pattern to use anyway; look at `NSMutableString` instead. – trojanfoe May 10 '16 at 14:33
  • I did try MutableStrings as one of my failed attempts, but that seemed to take up more memory if anything. I'll have another play now I have that golden nugget of a string. I never even considered it as I thought it would propagate down the functions. D'oh. – user2729972 May 10 '16 at 14:52
  • Every time you call `stringWithFormat`, you're telling the run time to create a new instance. – Tom Harrington May 10 '16 at 15:32

2 Answers2

5

Your calledFunction function creates an autoreleased NSString that won't be released until the current autorelease pool gets drained.

Your process function calls the calledFunction 100,000 times in a loop. During the duration of this loop, the current autorelease pool is not given a chance to drain. By the time the end of the process method is reached, all 100,000 instances of the NSString objects created in calledFunction are still in memory.

A common solution to avoid a build-up of autoreleased objects when using a large loop is to add an additional autorelease pool as shown below:

void process() {
    for (int i=0; i<targetCount; i++) {
        @autoreleasepool {
            calledFunction(i);
        }
    }
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
1

Your problem stems from the auto release pool, a somewhat anachronistic feature in the days of ARC.

When an object is created with an alloc/init combination the resultant object is owned by the caller. The same is true for the standard new method, it is defined as alloc followed by init.

For each init... method a class by have a matching <type>... method, this is defined as alloc/init/autorelease and returns an unowned object to the caller. For example your code uses stringWithFormat: which is defined as alloc/initWithFormat/autorelease.

The unowned returned object is in the auto release pool and unless ownership is taken it will be release automatically the next time that pool is emptied, which for the main autorelease pool is once per iteration of the main event loop. In many programs iterations of the event loop are frequent enough to reclaim objects from the auto release pool quick enough that memory usage does not climb. However if objects are created and then discarded a lot with a single iteration of the event loop, as in your example of a large for loop, the auto release pool can end up with a lot of needed objects before it is emptied.

A Common Solution

A common solution to this problem is to use a local auto release pool, which is emptied as soon as it is exited. By judicious placement of such local auto release pools memory use can be minimised. A common place for them is wrapping the body of loops which generate a lot of garbage, in your code this would be:

void process()
{
   for (int i=0; i<targetCount; i++)
   {  @autoreleasepool
      {
         calledFunction(i);
      }
   }
}

here all auto released and discarded objects created by calledFunction() will be reclaimed on every iteration of the loop.

A disadvantage of this approach is determining the best placement of the @autoreleasepool constructs. Surely in these days of automatic referencing count (ARC) this process can be simplified? Of course...

Another Solution: Avoid The Auto Release Pool

The problem you face is objects ending up in the auto release pool for too long, a simple solution to this is to never put the objects in the pool in the first place.

Objective-C has a third object creation pattern new..., it is similar to the <type>... but without the autorelease. Originating from the days of manual memory management and heavy auto release pool use most classes only implement new - which is just alloc/init - and no other members of the family, but you can easily add them with a category. Here is newWithFormat:

@interface NSString (ARC)

+ (instancetype)newWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);

@end

@implementation NSString (ARC)

+ (instancetype)newWithFormat:(NSString *)format, ...
{
   va_list args;
   va_start(args, format);
   id result = [[self alloc] initWithFormat:format arguments:args];
   va_end(args);
   return result;
}

@end

(Due to the variable arguments this is more involved than most new family methods would be.)

Add the above to your application and then replace calls to stringWithFormat with newWithFormat and the returned strings will be owned by the caller, ARC will manage them, they won't fill up the auto release pool - they will never enter it, and you won't need to figure out where to place @autoreleasepool constructs. Win, win, win :-)

HTH

CRD
  • 52,522
  • 5
  • 70
  • 86