Skip to content

[SR-12125] String(decoding:from:) doesn't try withContiguousStorageIfAvailable #54560

@weissi

Description

@weissi
Previous ID SR-12125
Radar rdar://problem/59148099
Original Reporter @weissi
Type Bug

Attachment: Download

Environment

swift 5.1 and 5.2 branch

Additional Detail from JIRA
Votes 1
Component/s Standard Library
Labels Bug, Performance
Assignee @milseman
Priority Medium

md5: 4785e0cdc39c1c82c168b48e24fef33d

Issue Description:

String(decoding:from🙂 does sometimes make intermediate Array copies of the Collection that it receives. That makes sense because the collection might not be contiguous. If however the collection implements withContiguousStorageIfAvailable, then String(decoding:from🙂 should use this.

The attached program shows this (parts reproduced here):

struct MyCollection: Collection {
    let storage: [UInt8]
    typealias Element = UInt8
    typealias Index = Array<UInt8>.Index
    typealias SubSequence = Array<UInt8>.SubSequence
    typealias Indices = Array<UInt8>.Indices

    init(underlying: [UInt8]) {
        self.storage = underlying
    }

    public var indices: Indices {
        return self.storage.indices
    }
    public subscript(bounds: Range<Index>) -> SubSequence {
        return self.storage[bounds]
    }

    public subscript(position: Index) -> Element {
        return self.storage[position]
    }

    public var startIndex: Index {
        return self.storage.startIndex
    }

    public var endIndex: Index {
        return self.storage.endIndex
    }

    func index(after i: Index) -> Index {
        return self.storage.index(after: i)
    }

    func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<UInt8>) throws -> R) rethrows -> R? {
        return try self.storage.withUnsafeBufferPointer(body)
    }
}

func makeString<Bytes: Collection>(_ bytes: Bytes) -> String where Bytes.Element == UInt8 {
    return String(decoding: bytes, as: Unicode.UTF8.self)
}

func testMyCollectionUnsafe(_ array: MyCollection) -> String {
    return array.withContiguousStorageIfAvailable {
        return makeString($0)
    }!
}

func testMyCollection(_ array: MyCollection) -> String {
    return makeString(array)
}

@inline(never)
func doMyCollectionUnsafe() {
    let array: [UInt8] = Array(repeating: UInt8(ascii: "X"), count: 15)
    let myCollection = MyCollection(underlying: array)

    for _ in 0..<10000 {
        precondition(testMyCollectionUnsafe(myCollection) == "XXXXXXXXXXXXXXX")
    }
}

@inline(never)
func doMyCollection() {
    let array: [UInt8] = Array(repeating: UInt8(ascii: "X"), count: 15)
    let myCollection = MyCollection(underlying: array)

    for _ in 0..<10000 {
        precondition(testMyCollection(myCollection) == "XXXXXXXXXXXXXXX")
    }
}

doMyCollection()
doMyCollectionUnsafe()

If we look at all allocations that happen more than once we see only

              libsystem_malloc.dylib`malloc
              libswiftCore.dylib`swift_slowAlloc+0x19
              libswiftCore.dylib`swift_allocObject+0x27
              test`specialized _ContiguousArrayBuffer.init(_uninitializedCount:minimumCapacity:)+0x39
              test`specialized Collection._copyToContiguousArray()+0x4f
              test`specialized makeString<A>(_:)+0x11a
              test`doMyCollection()+0xb2
              test`main+0x13
              libdyld.dylib`start+0x1
              test`0x1
            10000

so doMyCollection allocates 10,000 times but doMyCollectionUnsafe does not.

repro

swiftc -O test.swift && sudo ~/devel/swift-nio/dev/malloc-aggregation.d -c ./test

Metadata

Metadata

Assignees

Labels

bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.performancestandard libraryArea: Standard library umbrella

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions