NSData.data vs Data.withUnsafeBytes

It isn't, but that's beside the point - it is potentially incorrect, as it may violate strict aliasing (which isn't something Swift invented; it also exists in C, where the correct behaviour is likewise to memcpy the bytes of incompatible types). Violating strict aliasing is undefined behaviour.

In this case, you don't even need to use assumingMemoryBound - NSData.bytes returns an UnsafeRawPointer. The standard library provides an UnsafeRawBufferPointer type which gives you a Collection interface to the bytes (it will do loads/stores behind the scenes, but hey - it's there, so why not use it?).

Of course, the best thing to do is use the withUnsafeBytes method that Data provides, instead of building it yourself with the combination of withExtendedLifetime and manually creating an UnsafeRawBufferPointer.

Data.subscript is meaningfully slower, though it's unlikely your program will notice.

The fastest safe subscript in Swift is Array.subscript, which in the very best case is exactly as fast as UnsafeRawPointer.load, but generally speaking is very slightly slower (due to the bounds checking). Data.subscript is substantially more expensive than Array.subscript because Data is substantially more complex than Array, and so has substantially more checking to do in its subscript.

You can see this in Compiler Explorer.

indeed. Data subscript is about 3x slower and NSData subscript is about 260x times slower. other methods are more or less the same:

import Foundation

let size = 1000_000
let iterationCount = 100_000_000

func test_baseline() {
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= UInt8(index & 0xFF)
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_baseline time: \(Int(elapsed*1000))ms, \(r)")
}

func test_data_subscript() {
    var data = Data()
    for _ in 0 ..< size {
        data.append(UInt8.random(in: 0...0xFF))
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= data[index]
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_data_subscript time: \(Int(elapsed*1000))ms, \(r)")
}

func test_data_method_withUnsafeBytes() {
    var data = Data()
    for _ in 0 ..< size {
        data.append(UInt8.random(in: 0...0xFF))
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    data.withUnsafeBytes { bytes in
        for i in 0 ..< iterationCount {
            let index = (i * 0xdeadbeef) % size
            r ^= bytes[index]
        }
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_data_method_withUnsafeBytes time: \(Int(elapsed*1000))ms, \(r)")
}

func test_data_method_withUnsafeBytes_baseAddress_assumingMemoryBound() {
    var data = Data()
    for _ in 0 ..< size {
        data.append(UInt8.random(in: 0...0xFF))
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    data.withUnsafeBytes {
        let bytes = $0.baseAddress!.assumingMemoryBound(to: UInt8.self)
        for i in 0 ..< iterationCount {
            let index = (i * 0xdeadbeef) % size
            r ^= bytes[index]
        }
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_data_method_withUnsafeBytes_baseAddress_assumingMemoryBound time: \(Int(elapsed*1000))ms, \(r)")
}

func test_data_method_withUnsafeBytes_load() {
    var data = Data()
    for _ in 0 ..< size {
        data.append(UInt8.random(in: 0...0xFF))
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    data.withUnsafeBytes { bytes in
        for i in 0 ..< iterationCount {
            let index = (i * 0xdeadbeef) % size
            r ^= bytes.load(fromByteOffset: index, as: UInt8.self)
        }
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_data_method_withUnsafeBytes_load time: \(Int(1000*elapsed))ms, \(r)")
}

func test_nsdata_subscript() {
    let mutableData = NSMutableData()
    for _ in 0 ..< size {
        var b = UInt8.random(in: 0...0xFF)
        mutableData.append(&b, length: 1)
    }
    let nsdata = mutableData as NSData
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= nsdata[index]
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_nsdata_subscript time: \(Int(elapsed*1000))ms, \(r)")
}

func test_nsdata_bytes_assumingMemoryBound() {
    let nsdata = NSMutableData()
    for _ in 0 ..< size {
        var b = UInt8.random(in: 0...0xFF)
        nsdata.append(&b, length: 1)
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    let bytes = (nsdata as NSData).bytes.assumingMemoryBound(to: UInt8.self)
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= bytes[index]
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_nsdata_bytes_assumingMemoryBound time: \(Int(elapsed*1000))ms, \(r)")
}

func test_nsdata_bytes_load() {
    let nsdata = NSMutableData()
    for _ in 0 ..< size {
        var b = UInt8.random(in: 0...0xFF)
        nsdata.append(&b, length: 1)
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    let bytes = (nsdata as NSData).bytes
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= bytes.load(fromByteOffset: index, as: UInt8.self)
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_nsdata_bytes_load time: \(Int(elapsed*1000))ms, \(r)")
}

func tests() {
    test_baseline()
    test_data_subscript()
    test_data_method_withUnsafeBytes()
    test_data_method_withUnsafeBytes_baseAddress_assumingMemoryBound()
    test_data_method_withUnsafeBytes_load()
    test_nsdata_subscript()
    test_nsdata_bytes_assumingMemoryBound()
    test_nsdata_bytes_load()
}

tests()

test_baseline time: 70ms, 0
test_data_subscript time: 540ms, 0
test_data_method_withUnsafeBytes time: 193ms, 0
test_data_method_withUnsafeBytes_baseAddress_assumingMemoryBound time: 186ms, 0
test_data_method_withUnsafeBytes_load time: 185ms, 0
test_nsdata_subscript time: 49116ms, 0
test_nsdata_bytes_assumingMemoryBound time: 181ms, 0
test_nsdata_bytes_load time: 182ms, 0

1 Like

same array subscript test for completeness:

func test_array_subscript() {
    var array: [UInt8] = []
    for _ in 0 ..< size {
        array.append(UInt8.random(in: 0...0xFF))
    }
    var r: UInt8 = 0
    let start = CFAbsoluteTimeGetCurrent()
    for i in 0 ..< iterationCount {
        let index = (i * 0xdeadbeef) % size
        r ^= array[index]
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("test_array_subscript time: \(Int(elapsed*1000))ms, \(r)")
}

test_array_subscript time: 205ms, 0

(all tests were performed on intel macOS release target)

Hi, I'm new to swift but not to obj c. Because I like the simplified syntax of swift I started converting one of my projects to swift. Not entirely painless but conversion was going well until I came to NSData and found that Apple is doing quite the opposite of simplified syntax for handling Data and from the documentation, it seems to be a moving target.

One problem I have is that most of the documentation assumes that the data is some type of array. My application type the data to a struct as follows:

typedef enum : uint8_t {
DiscoveryPacketType,
ConnectPacketType,
...
} PacketType;

// Link level packet
typedef struct attribute ((packed)) {
PacketType packetType : 8;
uint packetLength : 16;
uint linkLCN : 8;
} lPkt;

  • ( void )
    deliverPacket: (NSData*) dataPacket {

lPkt* pkt = (lPkt*) dataPacket.bytes;
switch ( pkt->packetType ) {
case DiscoveryPacketType: {
dPkt" pktD = (dPkt*) dataPacket.bytes;
...

So the syntax is straightforward in obj c even if unsafe.

It seems to me that an equivalent syntax in swift could be equally straightforward. How about:
guard let pkt = data as lPkt
else { return }
switch pkt.pointee.packetType

Allowing the data to be cast as a struct would return a typed pointer. The pointer would retain the Data object so ARC would be happy.

How am I wrong?
Bob Rice

try like this in swift:

func deliverPacket(_ dataPacket: Data) {
    dataPacket.withUnsafeBytes { p in
        let packetType = p.load(as: PacketType.self)
        switch packetType {
        case .discovery:
            let discoveryPacket = p.load(as: DiscoveryPacket.self)
        case .connect:
            let connectPacket = p.load(as: ConnectPacket.self)
        }
    }
}

note, if you can express your C structs without C-bitfields - the structs can be bridged to swift keeping the correct field byte-alignment.

lPkt.stride: 4
lPkt.offset(of: packetType): 0
lPkt.offset(of: packetLength): 1
lPkt.offset(of: linkLCN): 3

which brings up a side question for those who can answer: how to specify packed alignment in pure swift without C bridging?

struct LinkPacket {
    let packetType: PacketType // 8 bits
    let packetLength: UInt16 // this needs to start at offset=1
    let linkLCN: UInt8 // this needs to start at offset=3
} // size must be 4

as another side note: the biggest change between Data and NSData (same for array and dictionaries) is value vs reference semantic.

how to specify packed alignment in pure swift without C bridging?

There’s no way to do this, but that’s not because of the packed part of your question but rather because Swift has no way to enforce any sort of structure layout. If you want to a structure with a known layout, you must import it from C.

With regards your deliverPacket(_:) example, be aware that load(as:) requires that the pointer be aligned, trapping if it’s not. For example, assuming this C structure:

struct TwoUInt8sAndAUInt16 {
    unsigned char u8a;
    unsigned char u8b;
    unsigned short u16;
};

which is laid out how you’d expect:

print(MemoryLayout<TwoUInt8sAndAUInt16>.size)               // 4
print(MemoryLayout<TwoUInt8sAndAUInt16>.alignment)          // 2
print(MemoryLayout.offset(of: \TwoUInt8sAndAUInt16.u8a)!)   // 0
print(MemoryLayout.offset(of: \TwoUInt8sAndAUInt16.u8b)!)   // 1
print(MemoryLayout.offset(of: \TwoUInt8sAndAUInt16.u16)!)   // 2

this code works:

let d = Data([0x01, 0x02, 0x03, 0x04, 0x05])
d.withUnsafeBytes { buf in
    print(buf.baseAddress!.load(as: TwoUInt8sAndAUInt16.self))
}

but this code traps:

d.dropFirst().withUnsafeBytes { buf in
    print(buf.baseAddress!.load(as: TwoUInt8sAndAUInt16.self))
}

Coming back to Robert Carl Rice’s original issue, the best way (IMO) to handle this is to avoid writing unsafe pointers but instead write a proper parser. There’s not enough info in their post to show this exactly, so here’s an example inspired by it:

enum Packet {
    case hello(ServiceID, String)
    case goodbye(ServiceID)
    
    struct ServiceID {
        var rawValue: UInt16
    }
}

func parsePacket(data: inout Data) -> Packet? {
    guard let tag = data.popFirst() else { return nil }

    guard
        let b1 = data.popFirst(),
        let b2 = data.popFirst()
    else { return nil }
    let serviceID = Packet.ServiceID(rawValue: UInt16(b1) * 256 + UInt16(b2))
    
    switch tag {
    case 0x01:
        guard let nameCount = data.popFirst() else { return nil }
        guard data.count >= nameCount else { return nil }
        let nameBytes = data.prefix(Int(nameCount))
        data.removeFirst(Int(nameCount))
        guard let name = String(bytes: nameBytes, encoding: .utf8) else { return nil }
        return Packet.hello(serviceID, name)
    case 0x02:
        return Packet.goodbye(serviceID)
    default: return nil
    }
}

While this seems wordy, it’s easy to write some helpers that shrink it down. My favourite example of this is Soroush Khanlou’s Regexes vs Combinatorial Parsing article.

This approach has some key advantages:

  • It does not rely on how the compiler lays out structures, something that’s not allowed for in Swift and is challenging even in C.

  • It handles endianness correctly.

  • It handle things that are tricky to represent in C, like variable-length strings.

  • It will perform better than you expect (-:

  • It’s memory safe.

Do not underestimate the value of that last point. It’s clear that the memory unsafeness inherent to C-based languages is a major cause of security vulnerabilities, and to get out from under that you have to use Swift safely. This is particularly important when, as shown in this example, you’re working with data coming off the network.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

6 Likes

interestingly adding __attribute__ ((__packed__)) to the struct prevents the crash.

perhaps in his example yes. there are instances though when it is invaluable to have memory layout matching disk/network layout as close as possible to avoid or minimize conversion cost. e.g. a ply format, which is an array of potentially millions elements where each element is a struct with vertex indices / vertex coordinates.

Hi, Thanks for the replies. You have confirmed my suspicion that swift has dropped support for handling packed data structures and the dataPacket.withUnsafeBytes is still a mess compared to the obj c equivalent code.

i would disagree on both qualifiers: "still" (don't expect any major changes here) and "a mess" (it is not). compared to C it is intentionally much harder (but not impossible) to write unsafe code in swift.

the fragment p.load(as: DiscoveryPacket.self) above is not that much different compared to your imaginary guard let pkt = data as lPkt or the obj-c version.

I still don't understand what all your syntax does and I doubt that most other readers do either.

It hasn’t dropped support for anything - Swift never had a native way to declare a packed data structure. AFAIK, it is something we want to support one day, but it has just been a low priority because it has a simple work-around (import the declaration from C).

As for handling type-punned arrays/data buffers, it’s relatively straightforward to write a struct which wraps the Data/Array/whatever you use to store your bytes, and which conforms to Collection by loading the desired data type (basically something like UnsafeRawBufferPointer, but loading a different type rather than UInt8). I think that’s also something we want to add to the standard library at some point, but again, it has just been a low priority thus far.

1 Like

Thanks Karl, I suspected that was the case since I didn't get a compile error from swift referencing data in my packed structure.

there are instances though when it is invaluable to have memory layout
matching disk/network layout as close as possible to avoid or minimize
conversion cost.

Perhaps. But do keep in mind the following:

  • Swift has strict rules about pointer manipulation and the compiler is increasingly enforcing that. So you can’t get away with, say, loading from a misaligned pointer, even that’s supported by all the current CPUs that Swift targets.

    If you haven’t already watched it, I can highly recommend WWDC 2020 Session 10167 Safely manage pointers in Swift.

  • On the plus side, Swift’s ‘zero cost’ abstractions mean that a lot of obvious wins in C are not necessarily wins in Swift. In many cases you can have both performance and safety.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

do you say we should not load from misaligned memory even if that works today with __attribute__ ((__packed__)), as that might crash in the future?

found this thread that sheds more light about it: https://forums.swift.org/t/best-practice-for-parsing-heterogeneous-types-from-data-in-swift-5

you can add an extension:

extension UnsafeRawPointer {
  func loadUnaligned<T>(as: T.Type) -> T {
    assert(_isPOD(T.self)) // relies on the type being POD (no refcounting or other management)
    let buffer = UnsafeMutablePointer<T>.allocate(capacity: 1)
    defer { buffer.deallocate() }
    memcpy(buffer, self, MemoryLayout<T>.size)
    return buffer.pointee
  }
}

with the help of this extension Quinn's example can work even without relying on __attribute__((__packed__)) added to the C struct (in fact it can work on Swift struct if the packed aspect is not needed otherwise):

d.dropFirst().withUnsafeBytes { buf in
    print(buf.baseAddress!.loadUnaligned(as: TwoUInt8sAndAUInt16.self))
}

also this from https://github.com/apple/swift-evolution/blob/master/proposals/0107-unsaferawpointer.md#future-improvements-and-planned-additive-api:

1 Like

Right. It's worth noting that, if you're a bit paranoid about adding allocations (which I am, I'll admit it), and you can construct a value or pass one inout, you can use withUnsafeMutableBytes(of:) to overwrite the value on the stack.

For instance, I use this for loading FixedWidthIntegers, which can be constructed from integer literals such as 0 (an _isPOD would also be good to add here).

extension UnsafeRawPointer {

  @inlinable @inline(__always)
  internal func loadUnaligned<T>(fromByteOffset offset: Int = 0, as: T.Type) -> T where T: FixedWidthInteger {
    var val: T = 0
    withUnsafeMutableBytes(of: &val) {
      $0.copyMemory(from: UnsafeRawBufferPointer(start: self + offset, count: MemoryLayout<T>.stride))
    }
    return val
  }
}

I don't believe there is a difference between memcpy and calling copyMemory on a mutable raw pointer, except that memcpy requires importing the C standard library, which has a different name on every platform and is a total pain to import (Darwin, Glibc, "CRT" on Windows, and I think we also support Musl for WASM?).

@Andrew_Trick - is there a difference that you know of?

yep, there are many ways to do it without memory allocation, a few examples:

extension UnsafeRawPointer {
    func loadUnaligned<T>(as: T.Type, _ buffer: UnsafeMutablePointer<T>) -> T {
        assert(_isPOD(T.self))
        memcpy(buffer, self, MemoryLayout<T>.size)
        return buffer.pointee
    }
}

extension Data {
    func load<T>(fromByteOffset offset: Int = 0, as type: T.Type) -> T {
        withUnsafeBytes { bytes in
            (bytes.baseAddress! + offset).bindMemory(to: T.self, capacity: 1).pointee
        }
    }
}

extension Data {
    func load<T>(fromByteOffset offset: Int = 0, defaultValue: T) -> T {
        withUnsafeBytes { bytes in
            var value = defaultValue
            memcpy(&value, bytes.baseAddress! + offset, MemoryLayout<T>.size)
            return value
        }
    }
}

Well, the second one isn't actually an unaligned load - it's a pointer cast and dereference. Some CPUs will fault if you attempt to load unaligned data that way (e.g. I've seen it on a Raspberry Pi).

it can't break on anything iOS/macos/tvos/watchos running today, right?