swiftunsafe-pointers

How to properly translate C code to UnsafeSwift


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?


Solution

  • 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?)