0

I'm learning about Unsafe Swift and trying to see if I can translate some C algorithms into it. I've been using some C algorithms in my Swift code for a while in order to get super fast runtime execution speeds for large data sets, but I'd love to get similar speeds written entirely in Swift without needing to include C files.

For my first test, I translated parts of a LinkedList to UnsafeSwift. Here are the C and UnsafeSwift versions and their functions for adding elements to either end of the list:

// C Implementation
typedef struct LinkedList {
    struct LinkedListNode *head;
    struct LinkedListNode *tail;
} LinkedList;

typedef struct LinkedListNode {
    void *data;
    struct LinkedListNode *next;
} LinkedListNode;

void LinkedListInsertAtBeginning(struct LinkedList *list, void *newData) {
    LinkedListNode *node = malloc(sizeof(LinkedListNode));
    node->data = newData;
    node->next = list->head;
    
    list->head = node;
    if (list->tail == NULL) {
        list->tail = node;
    }
}

void LinkedListInsertAtEnd(struct LinkedList *list, void *newData) {
    if (list->head == NULL) {
        LinkedListInsertAtBeginning(list, newData);
    } else {
        LinkedListNode *node = malloc(sizeof(LinkedListNode));
        node->data = newData;
        node->next = NULL;
        list->tail->next = node;
        list->tail = node;
    }
}

// UnsafeSwift Implementation
class UnsafeLinkedList<Element> {
    struct Node {
        var data: UnsafePointer<Element>
        var next: UnsafeMutableRawPointer?
    }
    
    var head: UnsafeMutableRawPointer?
    var tail: UnsafeMutableRawPointer?

    func addElementToFront(_ element: Element) {
        let mutableData = UnsafeMutablePointer<Element>.allocate(capacity: 1)
        mutableData.initialize(to: element)
        
        var newNode = Node(data: mutableData)
        newNode.next = head
        let nodePointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Node>.stride, alignment: MemoryLayout<Node>.alignment)
        nodePointer.initializeMemory(as: Node.self, to: newNode)
        
        head = nodePointer
        if tail == nil {
            tail = head
        }
    }
    
    func addElementToEnd(_ element: Element) {
        let mutableData = UnsafeMutablePointer<Element>.allocate(capacity: 1)
        mutableData.initialize(to: element)
        
        let newNode = Node(data: mutableData)
        let nodePointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Node>.stride, alignment: MemoryLayout<Node>.alignment)
        nodePointer.initializeMemory(as: Node.self, to: newNode)
        
        if head == nil {
            head = nodePointer
            tail = head
        } else {
            tail.unsafelyUnwrapped.assumingMemoryBound(to: Node.self).pointee.next = nodePointer
            tail = nodePointer
        }
    }
}

The speed of execution in unit tests is about 20% slower than the C LinkedList. I'm kind of new to C and UnsafeSwift, so that's why I'm posting here for advice. Is there anything in my UnsafeSwift code that could be changed to improve its performance?

RL2000
  • 913
  • 10
  • 20
  • I suggest to use https://github.com/apple/swift-collections/blob/main/Documentation/Deque.md – Cy-4AH Apr 20 '23 at 15:02
  • And just as importantly for this, look at the source to see how Apple has done this: https://github.com/apple/swift-collections/tree/main/Sources/DequeModule (you'll note the use of ManagedBufferPointer) – Rob Napier Apr 20 '23 at 15:04

1 Answers1

1

First, I expect a lot of the problem here is that the test is quite unfair. The Swift code is making a copy of the data and taking responsibility for its memory management, which the C code is just holding a pointer to it, assuming that something else is managing the memory. If you're going to reproduce this unsafe approach to memory management, look at Unmanaged (or possibly ManagedBuffer). I expect almost all of your 20% is the fact that the Swift code calls allocate twice, while the C code calls malloc once.

It's impossible to say how to rewrite this correctly in Swift without knowing how you're ensuring the memory management is done correctly in C. Your example suggests all that work is done by the caller. So to make this equivalent to the C, you'd change the functions to accept an UnsafePointer<Element> rather than Element, and take out the extra allocate. (But, of course, this is very unsafe, and you must make sure all the other code is perfect, just like in C.)

That said, have you compared the speed of this written in normal Swift? Don't assume you can beat the optimizer just by throwing in "unsafe." Start with normal Swift, see what SIL it produces (swiftc -O -emit-sil), and explore improvements from there. It's unclear how you're testing the performance here. Micro-benchmarks are notoriously hard to design accurately. (And of course, just for completeness, you are building with -O, right?)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you! Looks like I should start learning about `ManagedBuffer`, and do a lot of digging in Apple's `Deque` implementation. Hopefully I'm not in over my head! – RL2000 Apr 20 '23 at 19:46
  • You're probably right about my tests. I'm just writing the classes in a Swift Package, and doing the tests by putting them in XCTestCase `measure { ... }` blocks. To measure the C code, I made a wrapper class to interface with it (that was the intention all along-- a wrapper to interface with C code, or another to interface with the the UnsafeSwift). I did also write a version in normal Swift. The speed results had the C version fastest, Unsafe version 20% slower, and SafeSwift version a little slower than that. – RL2000 Apr 20 '23 at 19:51
  • 1
    Your current approach in C is not particularly optimized for performance. Fundamentally, if you want this to be very high performance, you want to avoid making memory allocations from the OS (i.e. calling malloc). That can be extremely slow. Most well-tuned data structures make allocations in "chunks" (often doubling in size whenever they run out of space), and then manage the memory themselves. That's what ManagedBuffer is for. Deque is an excellent place to start studying this kind of coding in production Swift. – Rob Napier Apr 20 '23 at 20:05
  • Marking this answer as the correct one because it's sending me in the right direction. Thank you! – RL2000 Apr 20 '23 at 23:02