Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performance: Array.removeAll vs `= []`

Is there a difference in performance computationally and/or memory-wise between Swift Array/Dictionary primitive functions removeAll and init? Basically, I am asking, what are the pros and cons to resetting a mutable collection in Swift, and is one considered the recommended way to empty an Array/Dictionary?

// Scenario A
var myArray = ["one", "two", "three"]
myArray.removeAll()

// Scenario B
myArray = ["one", "two", "three"]
myArray = []
like image 897
NoodleOfDeath Avatar asked Jan 10 '19 16:01

NoodleOfDeath


2 Answers

The performance difference should not be significant as they do largely the same thing. Let’s look at the source code:

/// Removes all elements from the array.
///
/// - Parameter keepCapacity: Pass `true` to keep the existing capacity of
///   the array after removing its elements. The default value is
///   `false`.
///
/// - Complexity: O(*n*), where *n* is the length of the array.
@inlinable
public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) {
  if !keepCapacity {
    _buffer = _Buffer()
  }
  else {
    self.replaceSubrange(indices, with: EmptyCollection())
  }
}

There are several other implementations of this method for different types, but the pattern is always the same. If removeAll parameter keepCapacity is false, just reinitialize it, largely equivalent to saying myArray = [].

So, the only question is whether you want to preserve the capacity of the array after removing its elements or not (which you might do if you’re emptying a large array and about to repopulate with another array of the same size).


If you want, benchmark it. For example, add "Unit Test” target to your project, bumping up the number of iterations high enough to make the duration observable:

class MyAppTests: XCTestCase {

    func testPerformanceRemoveAll() {
        var countTotal = 0
        var myArray: [Int] = []

        self.measure {
            for _ in 0 ..< 1_000_000 {
                myArray = Array(repeating: 0, count: 1_000)
                myArray.removeAll(keepingCapacity: false)
                countTotal += myArray.count
            }
        }

        XCTAssertEqual(countTotal, 0)
    }

    func testPerformanceReinitialize() {
        var countTotal = 0
        var myArray: [Int] = []

        self.measure {
            for _ in 0 ..< 1_000_000 {
                myArray = Array(repeating: 0, count: 1_000)
                myArray = []
                countTotal += myArray.count
            }
        }

        XCTAssertEqual(countTotal, 0)
    }

}

With the following results:

Test Case '-[MyAppTests.MyAppTests testPerformanceReinitialize]' started.
/.../MyApp/MyAppTests/MyAppTests.swift:41: Test Case '-[MyAppTests.MyAppTests testPerformanceReinitialize]' measured [Time, seconds] average: 0.221, relative standard deviation: 6.559%, values: [0.264467, 0.216076, 0.216146, 0.216040, 0.216014, 0.216426, 0.216374, 0.215876, 0.216272, 0.216152], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
Test Case '-[MyAppTests.MyAppTests testPerformanceReinitialize]' passed (2.646 seconds).

Test Case '-[MyAppTests.MyAppTests testPerformanceRemoveAll]' started.
/.../MyApp/MyAppTests/MyAppTests.swift:26: Test Case '-[MyAppTests.MyAppTests testPerformanceRemoveAll]' measured [Time, seconds] average: 0.235, relative standard deviation: 6.712%, values: [0.282223, 0.229732, 0.229601, 0.229624, 0.229584, 0.229652, 0.229695, 0.229729, 0.229702, 0.229659], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
Test Case '-[MyAppTests.MyAppTests testPerformanceRemoveAll]' passed (2.602 seconds).

enter image description here

By the way, if you’re wondering about why I’m adding the totals after emptying the array, I’m just trying to make sure that I actually use the array after emptying it to ensure that the optimizer doesn’t optimize out the code doing the emptying. It’s not necessary in this case, but is prudent.

I also did the test with Int instead of String, as I’m not curious about the String overhead, but rather trying to focus on the Array behavior.

Bottom line, the performance difference was largely indistinguishable.

like image 80
Rob Avatar answered Nov 18 '22 19:11

Rob


As stated in the docs removeAll()

is a Complexity: O(n), where n is the length of the array.

So it would vary in speed based on the size of the array. As others mentioned though it would be premature optimization to pick one over the other at the expense of readability.

like image 35
rcedwards Avatar answered Nov 18 '22 20:11

rcedwards