Measure strong/weak ARC references impact - Impact

2019-05-31 07:06发布

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.

1条回答
▲ chillily
2楼-- · 2019-05-31 07:44

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.

查看更多
登录 后发表回答