How to define the memory size of a struct

With @const coming soon I would like to figure out if it is possible to somehow tell the compiler/swift runtime how much memory to allocate for a struct.

You can consider this (that compiles using the current snapshot from master)

protocol VectorSize {
    _const static var size: UInt { get }
}


struct Vector<DataType, Size> where DataType: SignedNumeric, Size: VectorSize {
    
    init(_ type: DataType.Type, of size: Size.Type) {}
}

enum _3: VectorSize {
    _const static let size: UInt = 3
}

enum _4: VectorSize {
    _const static let size: UInt = 3
}

let a4 = Vector(Float.self, of: _4.self)

What I would like to do is somehow inform the compiler/swift that Vector should allocate enough memory for MemoryLayout<DataType>.stride * Size.size. I could use a unsafeMutablePointer and allocate this myself in the init but this would then not be an inline memory allocation but rather a pointer to memory that is on the heap and not in the stack.

Since entire point of this is to improve perfomance I would hope to be able to have this allocate on the stack if possible. A bit like allocating a size size array in c.

You can only do this by storing objects that are the appropriate size. In this case, you could make _3 look like this:

struct _3<BackingType> {
    private var storage: (BackingType, BackingType, BackingType) 
}

This uses Swift tuples as fixed-size Arrays, which is currently the only such type in Swift.

2 Likes

If you want a fixed-size array in Swift, then you have two options. (Edit: as Nevin notes below, there are other options. This post only features two, though.)

  • The withUnsafeTemporaryAllocation functions (documentation here and here).
    These functions temporarily allocate an arbitrary amount of memory for the duration of a closure. These functions prefer stack allocation, though if you allocate enough memory (over 1024 bytes currently, though this is subject to change) it'll allocate on the heap to prevent stack overflow with large buffers.

  • Using a tuple where each element is the same type (e.g. (Float, Float, Float, Float)). Tuples can be allocated on the stack. They are guaranteed to be stored contiguously (see Safely manage pointers in Swift - WWDC20 - Videos - Apple Developer), so you can randomly access tuple elements by taking a pointer to the tuple and rebinding that pointer to the element type. Here's an example of a generic four-element array structure:

struct CollectionOfFour<Element> {
    var elements: (Element, Element, Element, Element)
    static var count: Int { 4 }
    
    init(_ element0: Element, _ element1: Element, _ element2: Element, _ element3: Element) {
        elements = (element0, element1, element2, element3)
    }
    
    func withUnsafePointerToElements<R>(_ body: (UnsafePointer<Element>) throws -> R) rethrows -> R {
        return try withUnsafePointer(to: elements) {
            return try $0.withMemoryRebound(to: Element.self, capacity: Self.count, body)
        }
    }
    
    mutating func withUnsafeMutablePointerToElements<R>(_ body: (UnsafeMutablePointer<Element>) throws -> R) rethrows -> R {
        return try withUnsafeMutablePointer(to: &elements) {
            return try $0.withMemoryRebound(to: Element.self, capacity: Self.count, body)
        }
    }
    
    subscript(index: Int) -> Element {
        get {
            precondition((0..<Self.count).contains(index), "Index out of range")
            
            return withUnsafePointerToElements {
                return $0[index]
            }
        }
        set {
            precondition((0..<Self.count).contains(index), "Index out of range")
            
            withUnsafeMutablePointerToElements {
                $0[index] = newValue
            }
        }
    }
    
    // You can use this method to avoid potential redundant range checks and avoid ARC/copy-on-write issues.
    mutating func withElement<R>(_ index: Int, _ body: (inout Element) throws -> R) rethrows -> R {
        precondition((0..<Self.count).contains(index), "Index out of range")
        
        return try withUnsafeMutablePointerToElements {
            return try body(&$0[index])
        }
    }
}
More code: conforming `CollectionOfFour` to `RandomAccessCollection` and `MutableCollection`
struct CollectionOfFour<Element> {
    var elements: (Element, Element, Element, Element)
    static var count: Int { 4 }
    
    init(_ element0: Element, _ element1: Element, _ element2: Element, _ element3: Element) {
        elements = (element0, element1, element2, element3)
    }
    
    func withUnsafePointerToElements<R>(_ body: (UnsafePointer<Element>) throws -> R) rethrows -> R {
        return try withUnsafePointer(to: elements) {
            return try $0.withMemoryRebound(to: Element.self, capacity: Self.count, body)
        }
    }
    
    mutating func withUnsafeMutablePointerToElements<R>(_ body: (UnsafeMutablePointer<Element>) throws -> R) rethrows -> R {
        return try withUnsafeMutablePointer(to: &elements) {
            return try $0.withMemoryRebound(to: Element.self, capacity: Self.count, body)
        }
    }
    
    mutating func withElement<R>(_ index: Int, _ body: (inout Element) throws -> R) rethrows -> R {
        precondition((0..<Self.count).contains(index), "Index out of range")
        
        return try withUnsafeMutablePointerToElements {
            return try body(&$0[index])
        }
    }
}

extension CollectionOfFour: RandomAccessCollection, MutableCollection {
    var startIndex: Int { 0 }
    var endIndex: Int { Self.count }
    
    subscript(index: Int) -> Element {
        get {
            precondition((0..<Self.count).contains(index), "Index out of range")
            
            return withUnsafePointerToElements {
                return $0[index]
            }
        }
        set {
            precondition((0..<Self.count).contains(index), "Index out of range")
            
            withUnsafeMutablePointerToElements {
                $0[index] = newValue
            }
        }
    }
    
    func index(before i: Int) -> Int {
        return i - 1
    }
    
    func index(after i: Int) -> Int {
        return i + 1
    }
    
    func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R? {
        return try withUnsafePointerToElements {
            return try body(UnsafeBufferPointer(start: $0, count: Self.count))
        }
    }
    
    mutating func withContiguousMutableStorageIfAvailable<R>(_ body: (inout UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R? {
        return try withUnsafeMutablePointerToElements { baseAddress in
            var buffer = UnsafeMutableBufferPointer(start: baseAddress, count: Self.count)
            defer { // buffer should not be changed within body
                assert(buffer.baseAddress == baseAddress)
                assert(buffer.count == Self.count)
            }
            return try body(&buffer)
        }
    }
}
1 Like

There is a third option:

(Several variations thereof are linked in that thread.)

1 Like

Just to be clear, does the guarantee that homogeneous tuples are contiguous mean that a tuple of homogeneous tuples whose elements are all the same type are also contiguous? And if so, do all sub-tuples need to be of the same order?

e.g. Is this still correct:

 typealias BigTuple<Element> = (
    (Element, Element, Element, Element),
    (Element, Element, Element, Element),
    // ... //
    (Element, Element, Element, Element),
    (Element, Element)
)
typealias BigFixedSizeArray<Element> = TupleArray<BigTuple<Element>, Element>

As long as it’s completely homogeneous, any internal tuple structure won’t change the layout.

3 Likes