Memory Management in Objective-C

Ivan Nemes Categories: Mobile development Date 30-Jan-2014
Memory Management In Objective C

Table of contents

    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.

    Objective C memory management with blocks, ARC and non-ARC

    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.

    Memory Management Test Description

    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.

    Memory Management 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 test Memory management without ARC and manual release Memory management with ARC Memory management with ARC and auto-release pool
    Test of loops 89.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 variables 12.7Mb 10.3Mb 10.7Mb
    Test of work in multiple threads 645.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 test Memory management without the ARC and manual objects' releases Memory management with the ARC Memory management with the ARC and the auto-release pool
    The test of loops 668.0Mb 31.4Mb 23.6Mb
    The test of local variables 22.1Mb 10.2Mb 10.2Mb
    The test of work in multiple threads 2.48Gb 66.0Mb 66.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.

    testloops_620x77.jpg

    Image 1. The testing of loops

    testlocalvariables_620x77.jpg

    Image 2. The testing of local variables

    testofthreads1_616x76.jpg

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

    testofthreads2_619x78.jpg

    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

    testofloopscomplex_620x65.jpg

    Image 5. The testing of loops

    testoflocalvcomplex_616x63.jpg

    Image 6. The testing of local variables

    testofthreadscomplex_620x64.jpg

    Image 7. The testing of work inside of threads

    Conclusions about the ARC and autorelease pools

    Based on tests' results made in here, and taking everything from Mac Developer site 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]; } }

    • ARC has showed good results when local variables are taken in consideration, even if we use strings as locals.
    • 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).
    • 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.

    ivan-nemes-_authorsphoto.png
    Ivan Nemes Software Developer

    Ivan is a perfectionist, and a competitive athlete, qualities that are reflected in his tendency to not only match industry standards, but consistently improve upon them.