Menu Menu
Memory Management in Objective-C

Memory Management in Objective-C

Due to some issues found inside of a few Objective-C projects, I started with examination of memory footprints which are made by Objective-C applications. Almost always, memory consumption kept growing as the time was passing, especially when applications worked fully loaded with lots of threads and large loops (which create a lot of temporary object) inside of them. The Mac developer library contains some useful information which indicates that autorelease block is necessary even though the ARC is used inside of a project. In the following text, series of tests are described. In general, three types of tests were performed in three different environments. The tests are:

  1. Testing of loops, in this test large loops are created and they are making a lot of temporary objects.
  2. Testing of local variables, in this test large number of functions is called which are creating a lot of local variables.
  3. Testing of work inside of multiple threads, in this test multiple threads are created (100 of them) and each of them creates a big set of local variables and calls functions which either perform large loops (with a lot of temporary objects) or create a lot of local variables by itself.

During the testing, three projects are created, each containing same set of tests but projects are differently configured. Those differences among projects are related to the memory management. Therefore, we have following project configurations:

  1. Project 1: Memory management without ARC, in this project ARC is not used and leaks are intentionally left. It means that release and auto-release calls are not made. A purpose of this project is to create a good reference for memory handling inside of the ARC environment with and without autorelease blocks.
  2. Project 2: Memory management with ARC, in this project ARC is used but autorelease blocks are not. Results made by this project, compared to the other projects' results, show when the ARC is actually efficient and where not.
  3. Project 3. Memory management with ARC and autorelease blocks, in this project, both ARC and autorelease blocks are used. A purpose of this project is to demonstrate when autorelease blocks should be used and where not (Because, there are situation in which autorelease block is not necessary and it will even increase a memory footprint a little).

And, in the end, two types of test data are used:

  1. Simple objects (native), this include: int, string, bool, array and similar.
  2. Complex objects (custom), this include objects which are instantiated from custom made classes. In the testing 'Person' class is used. It is simple data transfer class which contains 22 string properties.

Test descriptions

In this chapter details of the tests will be shown. Each project used during this testing is quite simple, all of them are COCOA applications and all of them are just calling one specific test method at the time (either test of loops, test of l. variables or test of threads) from main method. The test of loops looks like:

- (void)testLoops:(BOOL)testComplexObjects
{
    for (int i = 0; i < 15; i++)
    {
        if (testComplexObjects)
        {
            [self testLoopsWithComplexObjectsInternal];
        }
        else
        {
            [self testLoopsInternal];
        }
    }
}

- (void)testLoopsInternal
{
    NSMutableArray *array = [NSMutableArray array];
    for (int j = 0; j < 200000; j++)
    {
        @autoreleasepool    //this wrapper only exists in test with autorelease pool
        {
            NSString *string = [NSString stringWithFormat:@"string - %d", j];
            [array addObject:string];
        }
    }
}

- (void)testLoopsWithComplexObjectsInternal
{
    NSMutableArray *array = [NSMutableArray array];
    for (int j = 0; j < 200000; j++)
    {
        @autoreleasepool  //this wrapper only exists in test with autorelease pool
        {
            Person *person = [[Person alloc] init];
            [array addObject:person];
        }
    }
}

Please note that only project which should test autorelease blocks actually has autorelease block in the code. Therefore, for other two types of projects just imagine that this block doesn't exist. The test of local variables looks like:

- (void)testThreads
{
    dispatch_group_t dispatchGroup = dispatch_group_create();

    for (int i = 0; i < 100; i++)
    {
        dispatch_group_async(dispatchGroup, 
            dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                ^{
                    @autoreleasepool//this wrapper only exists in test with autorelease pool
                    {
                        int a = 2222, b = 1111, c = 3333, d = 333,
                                f = 333, g = 333, h = 333;
                   
                   //and so on, this variable declaration's block is pretty much the same
                        //as in the previous test, I will just leave dots to save some space…
                 //……. Declarations continue here 

                   if (testComplexObjects)
                        [self testLoopsWithComplexObjectsInternal];
                        else 
                                  [self testLoopsInternal];

                        [self testLocalVariables:testComplexObjects];
                
                     }
                });

        if (i % 10 == 0)
        {
            [NSThread sleepForTimeInterval:1.5];
        }
    }

    dispatch_group_wait(dispatchGroup, 120);

    NSLog(@"Work completed");
}

As you can see, we are creating 100 threads and there is a small suspension (relaxing) time included in loop just to demonstrate more realistic scenario of a thread usage in an application (And to avoid choking of a processor). The test results in this scenario are collected twice, firstly when job is completed (so, when 'Work completed' sentence is written in output), and secondly when cool-down period is completed, therefore when all threads are killed.

Test results

These are the test results for simple objects (string, int, bool, float, array and similar). Special results are added in table cells, and they are described in more details bellow the table.

Type of the testMemory management without ARC and manual releaseMemory management with ARCMemory management with ARC and auto-release pool
Test of loops89.8Mb (for strings as temp objects)

31.2Mb (for integer, double and float variables as temp object)
76.4Mb (*1* special result : 74.5Mb) - for string as temp objects

16.5Mb ( for integer, double and float variables as temp object)
26.5Mb (*2* special result: 34.5Mb) for string as temp objects

15.6Mb ( for integer, double and float variables as temp object)
Test of local variables12.7Mb10.3Mb10.7Mb
Test of work in multiple threads645.5Mb when job done
163.2Mb after a cool-down period
594.7Mb when job done
156.5Mb after a cool-down period
48.9Mb when job done
42.3Mb after a cool-down period

45.5Mb when job done *3* special result
42.7Mb after cool-down period - *4* special result

Special result *1* refers to the situation in which the string is declared above loop:

NSString *string;
for (int j = 0; j < 200000; j++)
{
    string = [NSString stringWithFormat:@"string - %d", j];
    [array addObject:string];
}

Special result *2* refers to the situation in which the string is declared above loop and auto-release block exists:

@autoreleasepool  {
    NSString *string;
    for (int j = 0; j < 200000; j++)
    {
       string = [NSString stringWithFormat:@"string - %d", j];
       [array addObject:string];
    }
}

Special result *3* and special result *4* refer to the situation in which there is no auto-release block directly inside of a thread (but auto-release blocks remain in all other loops). Result *3* refers to the moment when job is done and result *4* refers to the moment when a cool-down period is completed. This scenario looks like:

dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
           ^{
            //code without autorelease block
            };

The following table shows results for complex objects (A person object is used which contains 22 string properties).

Type of the testMemory management without the ARC and manual objects' releasesMemory management with the ARCMemory management with the ARC and the auto-release pool
The test of loops668.0Mb31.4Mb23.6Mb
The test of local variables22.1Mb10.2Mb10.2Mb
The test of work in multiple threads2.48Gb66.0Mb66.6Mb (with autorelease block directly added inside of thread block)

66.2(without autorelease block directly added inside of thread block)

These are the snapshots made from the Activity monitor during the tests of simple objects.

Image 1. The testing of loops

Image 2. The testing of local variables

Image 3. The testing of threads (just after a completed job)

Image 4. The testing of threads (after a cool-down period)

These are the snapshots made from the Activity monitor during the tests of complex objects

Image 5. The testing of loops

Image 6. The testing of local variables

Image 7. The testing of work inside of threads

Conclusion

Based on tests' results made in here, and taking everything from Mac Developer site: https://developer.apple.com/library/mac/documentation/cocoa/conceptual/memorymgmt/Articles/mmAutoreleasePools.html#//apple_ref/doc/uid/20000047-SW5 into account, we can make a few conclusions about the ARC and autorelease pools:

  1. The ARC memory management of temporary strings which are created inside of loops is not efficient. If other object types are created inside of the loop memory management is better but results are still better if you use autorelease pool. Therefore, if any loop creates lots of temporary objects (especially strings) it should use auto-release pool inside of it. The suggested approach is listed below. Also, take into account that declaring a string above a loop is less efficient then the approach bellow, either if you wrap it with autorelease block or don't.

    for (int j = 0; j < 200000; j++)
                 {
                   @autoreleasepool 
                   {
                      NSString *string = [NSString stringWithFormat:@"string - %d", j];
                      [array addObject:string];
                   }
                 }
  2. ARC has showed good results when local variables are taken in consideration, even if we use strings as locals.
  3. In almost any aspect ARC has showed that is successful in releasing of the complex objects (Even if they contain lots of string properties). Therefore, the autorelease block is not necessary (it will not make a big difference).
  4. The Mac Developer library says that we should always explicitly create an autorelease pool when we create some new threads. Probably, this is the case when we use NSThread class, or some similar. But on another side, as tests' results show, a threading by the Grand Central Dispatch support is very efficient in memory management. Therefore, when GCD is used no special treatment is required (except if case 1 is present in thread).

As a main conclusion, we could say that if the GCD is used as a threading support then only loops which work with a large number of temporary objects (especially strings) should be treated in special manner and each iteration step should be wrapped inside of the autorelease block. In the case if another threading support is used (as mentioned on MAC library site) additional testing are required and probably some additional memory management actions.

Latest blog posts
String concatenation in Java
How we have learned a valuable lesson about teamwork
Team Boost Sessions: Wake up and boost your knowledge
Quality as one of the values we believe in
iOS Architecture Workshop: Impressions and Thoughts