Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TinyArray #98

Merged
merged 12 commits into from
Jun 20, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

@_spi(IntegrationTests) import X509

func run(identifier: String) {
measure(identifier: identifier) {
var tinyArray = TinyArray<Int>()
tinyArray.append(1)
tinyArray.append(2)
tinyArray.append(3)
tinyArray.append(4)
tinyArray.append(5)
tinyArray.append(6)
tinyArray.append(7)
tinyArray.append(8)
return tinyArray.count
}
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@
//
//===----------------------------------------------------------------------===//

import X509
@_spi(IntegrationTests) import X509

func run(identifier: String) {
measure(identifier: identifier) {
return 0
var counts = 0
counts += TinyArray(CollectionOfOne(1)).count

do {
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
var array = TinyArray<Int>()
array.append(contentsOf: CollectionOfOne(1))
counts += array.count
}

return counts
}
}
1 change: 1 addition & 0 deletions Sources/X509/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ add_library(X509
"SEC1PrivateKey.swift"
"Signature.swift"
"SignatureAlgorithm.swift"
"TinyArray.swift"
"Verifier/AnyPolicy.swift"
"Verifier/CertificateStore.swift"
"Verifier/PolicyBuilder.swift"
Expand Down
315 changes: 315 additions & 0 deletions Sources/X509/TinyArray.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCertificates open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

/// ``TinyArray`` is a ``RandomAccessCollection`` optimised to store zero or one ``Element``.
/// It supports arbitrary many elements but if only up to one ``Element`` is stored it does **not** allocate separate storage on the heap
/// and instead stores the ``Element`` inline.
@_spi(IntegrationTests) public struct TinyArray<Element> {
@usableFromInline
enum Storage {
case one(Element)
case arbitrary([Element])
}

@usableFromInline
var storage: Storage
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
}

// MARK: - TinyArray "public" interface

extension TinyArray: Equatable where Element: Equatable {}
extension TinyArray: Hashable where Element: Hashable {}
extension TinyArray: Sendable where Element: Sendable {}

extension TinyArray: RandomAccessCollection {
@_spi(IntegrationTests) public typealias Element = Element

@_spi(IntegrationTests) public typealias Index = Int

@inlinable
@_spi(IntegrationTests) public subscript(position: Int) -> Element {
get {
self.storage[position]
}
}

@inlinable
@_spi(IntegrationTests) public var startIndex: Int {
self.storage.startIndex
}

@inlinable
@_spi(IntegrationTests) public var endIndex: Int {
self.storage.endIndex
}
}

extension TinyArray {
@inlinable
@_spi(IntegrationTests) public init(_ elements: some Sequence<Element>) {
self.storage = .init(elements)
}

@inlinable
@_spi(IntegrationTests) public init(_ elements: some Sequence<Result<Element, some Error>>) throws {
self.storage = try .init(elements)
}

@inlinable
@_spi(IntegrationTests) public init() {
self.storage = .init()
}

@inlinable
@_spi(IntegrationTests) public mutating func append(_ newElement: Element) {
self.storage.append(newElement)
}

@inlinable
@_spi(IntegrationTests) public mutating func append(contentsOf newElements: some Sequence<Element>){
self.storage.append(contentsOf: newElements)
}

@discardableResult
@inlinable
@_spi(IntegrationTests) public mutating func remove(at index: Int) -> Element {
self.storage.remove(at: index)
}

@inlinable
@_spi(IntegrationTests) public mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows {
try self.storage.removeAll(where: shouldBeRemoved)
}

@inlinable
@_spi(IntegrationTests) public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows {
try self.storage.sort(by: areInIncreasingOrder)
}
}

// MARK: - TinyArray.Storage "private" implementation

extension TinyArray.Storage: Equatable where Element: Equatable {
@inlinable
static func ==(lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.one(let lhs), .one(let rhs)):
return lhs == rhs
case (.arbitrary(let lhs), .arbitrary(let rhs)):
// we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array
// if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat!

return lhs == rhs

case (.one(let element), .arbitrary(let array)),
(.arbitrary(let array), .one(let element)):
guard array.count == 1 else {
return false
}
return element == array[0]

}
}
}
extension TinyArray.Storage: Hashable where Element: Hashable {
@inlinable
func hash(into hasher: inout Hasher) {
// same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801
hasher.combine(count)
for element in self {
hasher.combine(element)
}
}
}
extension TinyArray.Storage: Sendable where Element: Sendable {}

extension TinyArray.Storage: RandomAccessCollection {
@inlinable
subscript(position: Int) -> Element {
get {
switch self {
case .one(let element):
guard position == 0 else {
fatalError("index \(position) out of bounds")
}
return element
case .arbitrary(let elements):
return elements[position]
}
}
}

@inlinable
var startIndex: Int {
0
}

@inlinable
var endIndex: Int {
switch self {
case .one: return 1
case .arbitrary(let elements): return elements.endIndex
}
}
}

extension TinyArray.Storage {
@inlinable
init(_ elements: some Sequence<Element>) {
dnadoba marked this conversation as resolved.
Show resolved Hide resolved
self = .arbitrary([])
self.append(contentsOf: elements)
}

@inlinable
init(_ newElements: some Sequence<Result<Element, some Error>>) throws {
var iterator = newElements.makeIterator()
guard let firstElement = try iterator.next()?.get() else {
self = .arbitrary([])
return
}
guard let secondElement = try iterator.next()?.get() else {
// newElements just contains a single element
// and we hit the fast path
self = .one(firstElement)
return
}

var elements: [Element] = []
elements.reserveCapacity(newElements.underestimatedCount)
elements.append(firstElement)
elements.append(secondElement)
while let nextElement = try iterator.next()?.get() {
elements.append(nextElement)
}
self = .arbitrary(elements)
}

@inlinable
init() {
self = .arbitrary([])
}

@inlinable
mutating func append(_ newElement: Element) {
self.append(contentsOf: CollectionOfOne(newElement))
}

@inlinable
mutating func append(contentsOf newElements: some Sequence<Element>){
switch self {
case .one(let firstElement):
var iterator = newElements.makeIterator()
guard let secondElement = iterator.next() else {
// newElements is empty, nothing to do
return
}
var elements: [Element] = []
elements.reserveCapacity(1 + newElements.underestimatedCount)
elements.append(firstElement)
elements.append(secondElement)
elements.appendRemainingElements(from: &iterator)
self = .arbitrary(elements)

case .arbitrary(var elements):
if elements.isEmpty {
// if `self` is currently empty and `newElements` just contains a single
// element, we skip allocating an array and set `self` to `.one(firstElement)`
var iterator = newElements.makeIterator()
guard let firstElement = iterator.next() else {
// newElements is empty, nothing to do
return
}
guard let secondElement = iterator.next() else {
// newElements just contains a single element
// and we hit the fast path
self = .one(firstElement)
return
}
elements.reserveCapacity(elements.count + newElements.underestimatedCount)
elements.append(firstElement)
elements.append(secondElement)
elements.appendRemainingElements(from: &iterator)
self = .arbitrary(elements)

} else {
elements.append(contentsOf: newElements)
glbrntt marked this conversation as resolved.
Show resolved Hide resolved
self = .arbitrary(elements)
}

}
}

@discardableResult
@inlinable
mutating func remove(at index: Int) -> Element {
switch self {
case .one(let oldElement):
guard index == 0 else {
fatalError("index \(index) out of bounds")
}
self = .arbitrary([])
return oldElement

case .arbitrary(var elements):
defer {
self = .arbitrary(elements)
}
return elements.remove(at: index)

}
}

@inlinable
mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows {
switch self {
case .one(let oldElement):
if try shouldBeRemoved(oldElement) {
self = .arbitrary([])
}

case .arbitrary(var elements):
defer {
self = .arbitrary(elements)
}
return try elements.removeAll(where: shouldBeRemoved)

}
}

@inlinable
mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows {
switch self {
case .one:
// a collection of just one element is always sorted, nothing to do
break
case .arbitrary(var elements):
defer {
self = .arbitrary(elements)
}

try elements.sort(by: areInIncreasingOrder)
}
}
}


extension Array {
@inlinable
mutating func appendRemainingElements(from iterator: inout some IteratorProtocol<Element>) {
while let nextElement = iterator.next() {
append(nextElement)
}
}
}