0

I was wondering if (and how much) strong/weak references management have an impact on code execution, especially when freeing objects to which many classes might have a weak reference. At first I mistaken this for ARC, but it's not.

There's a similar question on the same topic, however they don't investigate performance impact or try to pull a number out of it.

Let me be clear: I'm not, in any way, suggesting that ARC or strong/weak might have a bad impact on performance, or say "don't use this". I LOVE this. I'm just curious about how efficient it is, and how to size it.

I've put together this piece of code to get an idea of the performance impact of strong/weak references in execution time.

import Foundation

class Experiment1Class {
    weak var aClass: Experiment1Class?
}

class Experiment2Class {
    var aClass: Experiment2Class?
}

var persistentClass: Experiment1Class? = Experiment1Class()
var nonWeakPersistentClass: Experiment2Class? = Experiment2Class()

var classHolder = [Experiment1Class]()
var nonWeakClassholder = [Experiment2Class]()

for _ in 1...1000 {
    let aNewClass = Experiment1Class()
    aNewClass.aClass = persistentClass
    classHolder.append(aNewClass)

    let someNewClass = Experiment2Class()
    someNewClass.aClass = nonWeakPersistentClass
    nonWeakClassholder.append(someNewClass)
}

let date = Date()
persistentClass = nil
let date2 = Date()

let someDate = Date()
nonWeakPersistentClass = nil
let someDate2 = Date()

let timeExperiment1 = date2.timeIntervalSince(date)
let timeExperiment2 = someDate2.timeIntervalSince(someDate)

print("Time: \(timeExperiment1)")
print("Time: \(timeExperiment2)")

This piece of code only measure the amount of time it takes to free an object and to set to nil all its references.

IF you execute it in Playground (Xcode 8.3.1) you will see a ratio of 10:1 but Playground execution is much slower than real execution, so I also suggest to execute the above code with "Release" build configuration.

If you execute in Release I suggest you set the iteration count to "1000000" at least, the way I did it:

  • insert the above code into file test.swift
  • from terminal, run swiftc test.swift
  • execute ./test

Being this kind of test, I believe the absolute results makes no to little sense, and I believe this has no impact on 99% of the usual apps...

My results so far shows, on Release configuration executed on my Mac:

Time: 3.99351119995117e-06
Time: 0.0

However the same execution, in Release, on my iPhone 7Plus:

Time: 1.4960765838623e-05
Time: 1.01327896118164e-06

Clearly this test shows there should be little to no concern for real impact on typical apps.

Here are my questions:

  • Can you think of any other way to measure strong/weak references impact on execution time? (I wonder what strategies the system put in place to improve on this)
  • What other metrics could be important? (like multi threading optimisation or thread locking)
  • How can I improve this?

EDIT 1

I found this LinkedList test to be very interesting for a few reasons, consider the following code:

//: Playground - noun: a place where people can play
import Foundation
var n = 0
class LinkedList: CustomStringConvertible {
    var count = n
    weak var previous: LinkedList?
    var next: LinkedList?
    deinit {
        // print("Muorte \(count)")
    }
    init() {
        // print("Crea \(count)")
        n += 1
    }
    var description: String {
        get {
            return "Node \(count)"
        }
    }

    func recurseDesc() -> String {
        return(description + " > " + (next?.recurseDesc() ?? "FIN"))
    }
}

func test() {
    var controlArray = [LinkedList]()
    var measureArray = [LinkedList]()

    var currentNode: LinkedList? = LinkedList()

    controlArray.append(currentNode!)
    measureArray.append(currentNode!)

    var startingNode = currentNode

    for _ in 1...31000 {
        let newNode = LinkedList()
        currentNode?.next = newNode
        newNode.previous = currentNode!
        currentNode = newNode

        controlArray.append(newNode)
        measureArray.append(newNode)
    }
    controlArray.removeAll()
    measureArray.removeAll()

    print("test!")
    let date = Date()
    currentNode = nil
    let date2 = Date()

    let someDate = Date()
    startingNode = nil
    let someDate2 = Date()

    let timeExperiment1 = date2.timeIntervalSince(date)
    let timeExperiment2 = someDate2.timeIntervalSince(someDate)

    print("Time: \(timeExperiment1)")
    print("Time: \(timeExperiment2)")
}

test()

I found the following (running in Release configuration):

  • I couldn't be able to run more than ~32000 iterations on my phone, it's crashing of EXC_BAD_ACCESS during deinit (yes, during DEINIT... isn't this weird)
  • Timing for ~32000 iteration is 0 vs 0.06 seconds, which is huge!

I think this test is very CPU intensive because nodes are freed one after the other (if you see the code, only the next one is strong, the previous is weak)... so once the first one is freed, the others fall one after the other, but not altogether.

Community
  • 1
  • 1
Andre
  • 1,135
  • 9
  • 20
  • FYI - the impact (if any) of using ARC is nothing compared to impact of using MRC in the form of vastly more time spent debugging memory issues and far more app crashes due to incorrect memory management. – rmaddy Apr 12 '17 at 19:02
  • OF course @maddy, I would never suggest not to use ARC. I was just wondering how efficient is the machine. Let me be more specific in my question. – Andre Apr 12 '17 at 19:03
  • You've given your own answer, so why not just stop? – matt Apr 12 '17 at 19:11
  • I ask three question at the end: other ways ARC may have impact, other metrics and improvement of the way I measure it @matt – Andre Apr 12 '17 at 19:12
  • 1
    I can explain why your linked list example crashes! It is because you stack has over flowed. as you mentioned, "so once the first one is freed, the others fall one after the other, but not altogether." the `node` must deinit(release) the `node->next` element first before itself is fully deinited(return from deinit method), so it will cause a recursive call chain. The more element you got in your linked list, the deeper your stack will be. At a certain point, you run out of your stack and crash! – landerlyoung Jul 23 '17 at 15:56

1 Answers1

1

There is really no such thing as automatic memory management. Memory is managed by retain and release regardless of whether you use ARC. The difference is who writes the code, you or the compiler. There is manual memory management code that you write, and there is manual memory management code that ARC writes. In theory, ARC inserts into your code exactly the same retain and release commands that you would have inserted if you had done this correctly. Therefore the difference in performance should be epsilon-tiny.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • True, let me be more specific about what I'm looking for.. editing my question now – Andre Apr 12 '17 at 19:15
  • 1
    @André Move on, for heaven's sake. – matt Apr 12 '17 at 19:16
  • In practice ARC is usually more conservative than human programmers about inserting refcounting calls, impacting performance somewhat. However, counteracting this, it also has a clever optimization it can do where it dynamically replaces "return [x autorelease];" followed by "[x retain];" in the caller, with no refcounting. So sometimes it ends up ahead, sometimes it ends up a bit behind. Only profiling tools can say for sure for any given test case. – Catfish_Man Apr 12 '17 at 19:18
  • @matt I see you consider this to be a stupid question, you better move on indeed – Andre Apr 12 '17 at 19:20
  • @Catfish_Man Correct, my answer is intended to take into account both stages. After optimization, ARC's inserted retain and release should be just what you would have done, assuming you didn't pull any "tricks". – matt Apr 12 '17 at 19:22