Xcode 7.2b4 (7C62b): [Swift] Array.withUnsafeMutableBufferPointer() leads to unnecessary? allocations

Originator:janoschhildebrand
Number:rdar://23629352 Date Originated:20-Nov-2015
Status:Open Resolved:
Product:Developer Tools Product Version:Xcode 7.2b4 (7C62b)
Classification:Performance Reproducible:Always
 
Summary:
The attached project demonstrates this issue using the following type:

public struct Container<Element> {
    var storage: [Element]
    
    public init() {
        storage = Array()
    }
    
    public mutating func append(value: Element) {
        storage.append(value)
    }
    
    public mutating func append2(value: Element) {
        storage.append(value)
        storage.withUnsafeMutableBufferPointer { _ in }
    }
    
    public mutating func removeLast() {
        storage.removeLast()
    }
    
    public mutating func removeLast2() {
        storage.withUnsafeMutableBufferPointer { _ in }
        storage.removeLast()
    }
}

The Container type is a very small wrapper for Array, supporting append and removeLast by passing these along to the underlying array.
There are two variants of these methods, one with and one without calls to Array.withUnsafeMutableBufferPointer(...) with empty closure bodies.

The project tests the performance of these methods by first inserting and then removing a number of elements into a Container.

As demonstrated by the 'Bug' target, somewhat surprisingly the (empty) call to Array.withUnsafeMutableBufferPointer(...) leads to a large performance penalty. Profiling indicates that this is due to object allocations, one per call to 'removeLast2()' in fact.

Interestingly this seems to be caused by an interaction of  'append2()' and 'removeLast2()' since, if the 'withUnsafeMutableBufferPointer()' call is removed in either of the functions the allocations do not occur.

I also have another weird interaction-leads-to-performance-difference case that is demonstrated in the 'Bug2' target. The code is almost the same as for 'Bug' but now the 'withUnsafeMutableBufferPointer()' call is also used in 'append' and 'removeLast':

public struct Container<Element> {
    var storage: [Element]
    
    public init() {
        storage = Array()
    }
    
    public mutating func append(value: Element) {
        storage.append(value)
        storage.withUnsafeMutableBufferPointer { _ in }
    }
    
    public mutating func append2(value: Element) {
        storage.append(value)
        storage.withUnsafeMutableBufferPointer { _ in }
    }
    
    public mutating func removeLast() {
        storage.withUnsafeMutableBufferPointer { _ in }
        storage.removeLast()
    }
    
    public mutating func removeLast2() {
        storage.withUnsafeMutableBufferPointer { _ in }
        storage.removeLast()
    }
}

In this case, the test using 'append' and 'removeLast' is much faster than the test using 'append2' and 'removeLast2' even though they are now 'equivalent'.
For additional fun, if the test using 'append2' and 'removeLast2' is not used (by uncommenting it), the test using 'append' and 'removeLast' now becomes slower.


Now I'm filing these two issues as a single bug as they are probably related. However, If you'd rather I file separate bugs, let me know and I will do so.

Steps to Reproduce:
1. Open the attached project

2. Build & run the 'Bug' target
3. Comment line '32' in main.swift of Bug -> Build & run

4. Build & run the 'Bug2' target
5. Comment line '78' in main.swift of Bug2 -> Build & run

Expected Results:
Now I don't know in which cases the allocation caused by 'withUnsafeMutableBufferPointer()' might actually be necessary, but given the fact that in 'Bug2' the compiler is able to produce code that does not incur the allocation, it should at least be able to do this in all these cases.

-> In steps 2-4, the printed runtimes should be almost equal.
-> In step 5, the first results should be almost equal to the results in step 4.

Also the interactions between the functions are extra weird and should probably? not happen... At least its very confusing when the performance of one function depends on whether another function is compiled along with it... ;-)

Actual Results:
For reference, these are the results I get on my machine:

Step 2:
0.13051700592041
2.01244086027145

Step 3:
0.126521706581116
0.147176027297974

Step 4:
0.167377114295959
2.01639533042908

Step 5:
2.00278067588806
0.000157773494720459


Version:
Xcode 7.2b4 (7C62b)
Apple Swift version 2.1.1 (swiftlang-700.1.101.13 clang-700.1.81)
OS X 10.11.1 (15B42)

Notes:


Configuration:
-O

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!