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:
- Testing of loops, in this test large loops are created and they are making a lot of temporary objects.
- Testing of local variables, in this test large number of functions is called which are creating a lot of local variables.
- 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:
- 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.
- 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.
- 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:
- Simple objects (native), this include: int, string, bool, array and similar.
- 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.
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
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:
-
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]; } }
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.