swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.16k stars 10.33k forks source link

[SR-10614] Assigning arrays of tuples to arrays of similar tuples surprisingly allocates. #53014

Open Lukasa opened 5 years ago

Lukasa commented 5 years ago
Previous ID SR-10614
Radar None
Original Reporter @Lukasa
Type Bug
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 0 | |Component/s | Compiler, Standard Library | |Labels | Bug, Performance | |Assignee | None | |Priority | Medium | md5: cc552939b1fbd98ddc37640577572edd

Issue Description:

The following code, when run, allocates 200,000 transient _ContiguousArrayStorage objects:

struct ArrayCounter {
    private var storage: [(name: String, value: String)]

    init(_ array: [(name: String, value: String)]) {
        self.storage = array
    }

    var count: Int {
        return self.storage.count
    }
}

@inline(never)
func countTester(_ array: [(String, String)]) -> Int {
    let arrayCounter = ArrayCounter(array)
    return arrayCounter.count
}

let array = [("hello", "world"), ("I am", "Array")]
var count = 0
for _ in 0..<200_000 {
    count += countTester(array)
}

print(count)

The allocation stack is

   0 libsystem_malloc.dylib malloc_zone_malloc
   1 libsystem_malloc.dylib malloc
   2 libswiftCore.dylib swift_slowAlloc
   3 libswiftCore.dylib _swift_allocObject_(swift::TargetHeapMetadata<swift::InProcess> const*, unsigned long, unsigned long)
   4 libswiftCore.dylib _swift_allocObject_(swift::TargetHeapMetadata<swift::InProcess> const*, unsigned long, unsigned long)
   5 NIOLoopPerformanceTester specialized ContiguousArray.reserveCapacity(_:) /Users/lukasa/Documents/Cocoa/NIOLoopPerformanceTester/<compiler-generated>:0
   6 NIOLoopPerformanceTester specialized _arrayForceCast<A, B>(_:) /Users/lukasa/Documents/Cocoa/NIOLoopPerformanceTester/<compiler-generated>:0
   7 NIOLoopPerformanceTester countTester(_:) /Users/lukasa/Documents/Cocoa/NIOLoopPerformanceTester/NIOLoopPerformanceTester/main.swift:23
   8 NIOLoopPerformanceTester main /Users/lukasa/Documents/Cocoa/NIOLoopPerformanceTester/NIOLoopPerformanceTester/main.swift:30
   9 libdyld.dylib start

The issue appears to be that the cast from [(String, String)] to [(name: String, value: String)] triggers an allocation and copy, despite the fact that the two types are sufficiently close to identical that the compiler will simply let you assign one to the other.

Either this should not allocate, or it should not be possible to assign the array of one type to a variable of the other.

belkadan commented 5 years ago

An Array carries its type around, so it'd be a stretch to share ownership here. But Swift does not have a convention that conversions that can cause allocations need to be explicit. The same is true for concrete type -> protocol.

Lukasa commented 5 years ago

This is definitely my lack of understanding here, but there are some cases in which this conversion does not need to allocate.

Storing [(String, String)] in a variable of [(name: String, value: String)]: allocates.
Storing [S] in a variable of [P] where protocol P and struct S: P: allocates.
Storing [S] in a variable of [Super] where class Super and class S: Super: does not allocate.

What don't I understand about why subclasses don't need a reallocation?

belkadan commented 5 years ago

I'm not saying this change can't be done, and the class case is a good point. I'm just saying "it should not be possible to assign the array of one type to a variable of the other" is not something that Swift plans to do. We had piles of complaints when [S] to [P] didn't work, even with that justification.

Lukasa commented 5 years ago

Oh sure, I totally understand that that's a genie that cannot be put back into the bottle.

I should note that this is low-priority for us: the case where we hit this issue is easily resolved and I'll be putting a patch up soon to fix it.