Meet Non-Copyable Types – Swift’s Secret Performance Boost

We explore non-copyable types, Swift’s game-changing feature that boosts performance and safety for resource-heavy code. Find out how non-copyable types work, why they matter, and how they can optimize your workflow for critical use cases.

Version 5.9 marked a big step in Swift’s evolution toward a high-performance and ergonomic programming language. Along with new ownership modifiers, the release introduced non-copyable types – instances that always maintain unique ownership, ensuring they cannot be copied. 

Although we likely won’t see non-copyable types widely used in common codebases, they enable important features like Swift-native Atomics and Locks. Non-copyable types also offer significant benefits in terms of safety and performance, but their somewhat esoteric syntax and niche use cases can make them difficult to grasp. 

In this article, we’ll take a closer look at what non-copyable types bring to the table, break down their complexities, and explain how they fit into Swift’s evolving ecosystem. Even if you don’t expect to use non-copyable types regularly, learning how they work will give you a better understanding of Swift’s growing capabilities.

Copyability in Swift

The concept of copyability represents one of the main distinguishing traits between value and reference types in Swift. This distinction is highlighted early in The Swift Programming Language book:

A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function. (source)

Unlike value types, reference types are not copied when they’re assigned to a variable or constant, or when they’re passed to a function. (source)

This distinction often plays a crucial role in choosing the right construct for solving a particular problem. However, the introduction of non-copyable types gives us a new perspective on copyability.

Consider the following code sample:

	var x: Test? = Test()
let y = x
x = nil

In this example, we deliberately avoid specifying what Test is. However, regardless of its exact definition, the compiler must guarantee that the variable y remains valid even after x is set to nil.

How can a compiler do that? Typically, it handles it through one of two mechanisms: 

1

If Test is a simple (bitwise-copyable) struct, its bytes are copied to a separate memory location.

2

If Test is a class, the instance is retained through reference counting.

These two approaches demonstrate different notions of copyability, which have been explicitly formalized with the introduction of the Copyable protocol. Both mechanisms allow programmers to pass values and references around safely and efficiently, while the compiler can use one of these operations to ensure safe code execution.

The introduction of the Copyable marker protocol formalizes an already implicit concept. All existing types in Swift implicitly conform to it, so this change adds no additional burden for developers.

Implicit conformance is not a new idea in Swift. We’ve already seen implicit Sendable conformance, and there is also a proposal for adding default conformance to the Escapable protocol.

Why we need non-copyable types

For over a decade, Swift’s ever-present copyability has served us well. But as Swift evolves into a true general-purpose programming language, and especially with growing ambitions in the embedded systems space, new demands for safety and performance have emerged. These requirements bring forth new code idioms that call for advanced features like non-copyable types, designed specifically for performance-critical and correctness-sensitive code. 

For example, some real-world scenarios where performance and correctness are critical include:

  • Database connections
  • Database cursors
  • Transactions
  • Network connections
  • Audio buffers
  • Wrapping C libraries 

In these cases, specific state transitions can invalidate instances, making ownership controls essential for ensuring safe and reliable code.

Let’s explore these two key aspects in more detail.

Optimizing performance

In a previous article on ownership in Swift, we explored the concept of borrowing and consuming parameter modifiers, both aimed at optimizing performance-critical code. Non-copyable types take this a step further by providing even stronger guarantees, requiring us to explicitly define ownership conventions when passing an instance of a non-copyable type.

There are two main sources of performance improvements when using non-copyable types:

  • Inline storage: non-copyable types are stored inline with the containing type, which eliminates the need for separate memory allocations
  • No reference counting: since non-copyable types don’t rely on reference counting, the overhead associated with tracking references is completely removed 

Consider the following code:

	func test(_ numbers: [Int]) {}

let numbers = (1...10000000).map { $0 }
test(numbers)

Since Array is a copy-on-write (CoW) data type and no mutations are happening, the large array won’t be copied – this is an important optimization for most common use cases.

However, there’s a hidden performance cost: reference counting. As a CoW data type, the underlying array buffer must be reference counted so that Swift’s runtime knows when an actual copy is necessary.

While a single retain might seem negligible, when this code is invoked in a loop, the performance impact becomes more significant – a point we’ll explore further in the case study.

Ideally, we’d have a NonCopyableArray type that would allow the compiler to reject code that might trigger expensive copies.

Maximizing correctness

Non-copyable types also improve code correctness by enforcing unique ownership and preventing invalid states. For example, consider the following code (the ~ indicates non, which we’ll explore in more detail later):

	struct SingleUseToken: ~Copyable {
    private let token: String

    init(token: String) {
        self.token = token
    }

    consuming func use() -> String {
        return token
    }
}

func run() {
    var token = SingleUseToken(token: "1234") 
    print(token.use())
    print(token.use()) ❌ `token` consumed more then once
}

By declaring SingleUseToken as ~Copyable, we ensure that the compiler rejects any invalid code where a token is consumed more than once.

This explicit state management requires unique ownership, and the compiler enforces the following:

There can’t be multiple variables pointing to the same value.

For example:

	func multipleVariables() {
    var token = SingleUseToken(token: "1234")
    var tokenReference = token
    print(token.use()) ❌ `token` consumed more then once
}

The message above is somewhat confusing, but the token is not valid anymore when we assign (move) it to tokenReference.

To summarize, non-copyable types represent a new tool in the Swift type system:

Unique resourceUnique ownershipInline storage
struct, enum
class, actor
~Copyable


The syntax of non-copyable types

Swift’s esoteric choice of syntax for non-copyable types has sparked considerable debate within the Swift community, and it’s worth understanding the rationale behind it.

The naming of this feature is somewhat unfortunate and has also caused some confusion, especially when used in Swift’s generics system. The main challenge was to establish a syntax that clearly communicates the suppression of default Copyable conformance.

The initial pitch for this feature proposed the following syntax:

	@noncopyable
struct Test {}

However, this approach didn’t integrate well with Swift’s generics system, so the language designers adopted a different method, using a protocol constraint instead.

Typically, protocols in Swift describe functionality that adds behavior to types. In the case of non-copyable types, the situation is reversed – functionality is removed from the type. Therefore, Swift needed a syntax that allows a type to suppress the default Copyable conformance.

Swift’s ~ operator is not generally applicable and only works on specifically chosen protocols. Nevertheless, it is a more flexible and powerful tool because it allows Swift to suppress other conformances in the future (e.g. BitwiseCopyable).

Interestingly, Swift is not alone in adopting this approach. For example, Rust uses the ? operator for a very similar purpose.

Ownership modifiers

New ownership modifiers are a key requirement for the implementation of non-copyable types. While for regular types these modifiers serve as optional guardrails, they are mandatory for non-copyable types because these types enforce unique ownership. This means developers must be explicit when passing them around, clearly defining how ownership rules are transferred or borrowed.

Consuming ownership

When a function takes ownership of a non-copyable type, we mark the parameter as consuming. This transfers ownership to the function, invalidating the original owner:

	func use(token: consuming SingleUseToken) {}

let token = SingleUseToken()
use(token: token)
token.use() ❌ `token` consumed more then once

Some operations, like variable assignments, have default consuming semantics:

	let token = SingleUseToken()
let consumedToken = token
token.use() ❌ `token` consumed more then once

Borrowing ownership

If a function requires a parameter of a non-copyable type only temporarily, we mark the parameter as borrowing. This indicates that the original ownership is not invalidated and that the function cannot consume the parameter in any way.

	func borrow(token: borrowing SingleUseToken) {}

let token = SingleUseToken()
use(token: token)
token.use() ✅ allowed


func borrow(token: borrowing SingleUseToken) {
    token.use() ❌ `token` is borrowed and cannot be consumed
}

let token = SingleUseToken()
borrow(token: token)

The inout modifier

Before looking into how the inout modifier behaves with non-copyable types, it’s helpful to review what inout does:

In-out parameters are passed as follows:

  • When the function is called, the value of the argument is copied.
  • In the body of the function, the copy is modified.
  • When the function returns, the copy’s value is assigned to the original argument.

For non-copyable types, things are a bit different. If a function consumes an inout argument, the compiler will issue a warning, indicating that the inout argument must be reinitialized (since it cannot be copied back after consumption):

	func `inout`(token: inout SingleUseToken) {
    token.use() ❌ Missing reinitialization of inout parameter `token` after consume
}

let token = SingleUseToken()
inout(token: token)

Mutating functions

A mutating function implicitly takes the self argument as inout, which means the same rules apply as for explicit inout arguments:

	extension SingleUseToken {
    mutating func useAndUpdate() {
        self.use() ❌ Missing reinitialization of inout parameter `self` after consume
    }
}

The discard operator

Although non-copyable types are value types, their unique ownership makes deinit relevant. 

You can declare a deinit for non-copyable types, just as you would for classes:

	struct SingleUseToken {
    deinit {
        UserDefaults.standard.set(false, "token-used")
    }
}

However, there are scenarios where you may want to prevent deinit from being called in certain code paths:

	struct SingleUseToken {

    consuming func use() -> String {
       let token = token
       UserDefaults.standard.set(true, "token-used")
       discard self
       return token
    }

    deinit {
        UserDefaults.standard.set(false, "token-used")
    }
}

If we didn’t discard self, the deinit would be called and token-used would be incorrectly set to false.

Non-copyable generics

Swift’s type hierarchy has traditionally assumed that types are copyable, which is reflected in the interaction between structs, enums, and classes:

However, the introduction of non-copyable types presents a new challenge: how do these types fit into the existing hierarchy?

At first glance, one might assume that non-copyable types could be considered subtypes of regular structs and enums. However, this isn’t possible because these types are inherently copyable. Another idea might be to place them as subtypes of Any, but unfortunately, this approach also fails since Any is implicitly Copyable

Consider the following example:

	struct Test: ~Copyable { }

let test = Test()
test as Any ❌ Noncopyable type 'Test' cannot be erased to copyable existential type 'Any'

Therefore, non-copyable types do not fit into the existing hierarchy but form their own type hierarchy instead.

However, having two independent type hierarchies makes it really challenging to write generic code. A simple example illustrates this issue:

	struct Test: ~Copyable { }

func generic<T>(t: T) {}

generic(Test()) ❌ Noncopyable type 'Test' cannot be substituted for copyable generic parameter 'T'

In this case, the generic type parameter T assumes copyability, as it is a subtype of Any. Consequently, a type from the non-copyable hierarchy can’t be passed in.

We could employ a workaround here, writing an alternative function that accepts non-copyable types. However, this approach makes non-copyable types feel like second-class citizens in Swift’s type system. The issue is even worse if you consider that you can’t use non-copyable types with Optional:

	struct Test: ~Copyable { }

let test = Optional<Test>.none ❌ Noncopyable type 'Test' cannot be used with generic type 'Optional<Wrapped>'

Before delving into the new type hierarchy, it is important to clarify the meaning of ~Copyable

~Copyable suppresses the implicit conformance to the Copyable protocol.

This means that when a type is marked as ~Copyable, the compiler is not allowed to insert any copy operations for that type.

Here’s what this means for generic code constrained to ~Copyable:

	func generic<T: ~Copyable>(t: T) { ❌ Parameter of noncopyable type 'T' must specify ownership
}

First of all, we need to specify ownership, as the compiler needs to understand which operations are allowed within the function.

	struct NC: ~Copyable { }
struct CO { }

func generic<T: ~Copyable>(t: borrowing T) {}

generic(Test()) ✅ allowed
generic(CO()) ✅ allowed

Once we do that, we can use generic functions both with ~Copyable and Copyable types.

This way, Copyable types are effectively a subtype of ~Copyable, as Copyable adds new functionality to the conforming type.

Finally, the new type hierarchy can be illustrated as follows:

Non-copyable or maybe-copyable?

At first glance, it may seem strange that we can use a copyable type in place of a non-copyable one in generic code. This is where the naming of the feature can be confusing. In generic code, we are not strictly dealing with non-copyable types, but rather maybe-copyable types.

In a way, the distinction is contextual:

  • Our Test struct is non-copyable – the compiler will prevent any attempt to copy it
  • A generic parameter T constrained by ~Copyable is maybe-copyable, meaning that a given parameter cannot be assumed to be copyable, but can be, depending on the specific type

Though it may seem like this distinction calls for a different syntax, in practice, both non-copyable and maybe-copyable types share the fundamental requirement: the compiler cannot assume the type is copyable.

Case study: finding the longest line in a large file

To demonstrate the performance advantages of non-copyable types in Swift, let’s try a real-world, performance-critical task: finding the longest line in a large file. In cases where files are too large to load entirely into memory, we need to read them line by line. Optimizing tasks like this can yield significant benefits for performance, especially when processing multi-gigabyte files.

For our case study, we use a large dataset – 2,47GB of Github Commit Messages. The goal is to identify the longest line in the file as efficiently as possible.

Initial approach: using high-level Swift APIs

The following code sample demonstrates a possible typical wrapper around a FILE pointer:

	public final class File {
    private let file: UnsafeMutablePointer<FILE>

    public init(fileURL: URL) throws {
        guard let file = fopen(fileURL.path, "r") else {
            throw NSError(
                domain: NSPOSIXErrorDomain,
                code: Int(errno),
                userInfo: nil
            )
        }
        self.file = file
    }

    deinit {
        let success = fclose(file) == 0
        assert(success)
    }
}

And this code illustrates the readLine API that the File wrapper might expose.

	public func readLine() throws -> String? {
    var buffer = [CChar](repeating: 0, count: maxLength)
    guard fgets(&buffer, Int32(maxLength), file) != nil else {
        if feof(file) != 0 {
            return nil
        } else {
            throw NSError(
                domain: NSPOSIXErrorDomain, 
                code: Int(errno), 
                userInfo: nil
            )
        }
    }
    return String(cString: buffer)
}

Finally, we can implement an algorithm to find the longest line:

	extension File {
    public func findLongest() throws -> String? {
        var longest: String? = nil
        while let line: String = try readLine() {
            if line.count > (longest?.count ?? 0) {
                longest = line
            }
        }
        return longest
    }
}

Running this code on a large file won’t complete in a reasonable amount of time. Before we optimize it, we want to check the instruments trace:

We can see that over 90% of the time is spent in functions related to String handling, which is somewhat expected. Swift’s Strings are very powerful, but that power comes at a cost.

Ideally, we’d want to see file reading operations at the top of the profile.

Optimization #1: using the CChar array instead of String

Instead of constructing a String for every line that we read, let’s expose the [CChar] array as a return type of readLine and construct a String only when we have a result:

	func findLongest() throws -> String? {
    var longest: [CChar]? = nil
    var longestCount: Int = 0
    while let line: [CChar] = try readLine() {
        let count = line.withUnsafeBufferPointer { strlen($0.baseAddress!) }
        if count > longestCount {
            longest = line
            longestCount = count
        }
    }
    return longest.map { String(cString: $0) }

Let’s see how it performs now.

This change eliminates much of the overhead associated with String creation, but performance is still impacted by the memory allocations for each line. This is expected, as we are allocating a new buffer for each line read.

Optimization #2: reusing existing buffers

Let’s reuse a buffer from the previous read iteration instead of allocating a new one every time:

	func findLongest() throws -> String? {
    var buffer = [CChar](repeating: 0, count: maxLength)
    var longest: [CChar]? = nil
    var longestCount: Int = 0
    while let line: [CChar] = try readLine(reuseChunk: &buffer) {
        let count = line.withUnsafeBufferPointer { strlen($0.baseAddress!) }
        if count > longestCount {
            longest = line
            longestCount = count
        }
    }
    return longest.map { String(cString: $0) }
}

If we profile this code, we get the following trace:

This reduces the memory allocation cost but surfaces a new issue: the Array type in Swift is a copy-on-write (CoW) data type, and whenever we find a new longest line, the whole array will be copied.

Optimization #3: introducing FileChunk

To avoid unnecessary copying, we will not expose [CChar] directly but wrap a pointer in a FileChunk object:

	public final class FileChunk {
    private let maxCount: Int
    let _buffer: UnsafeMutablePointer<CChar>

    init(maxCount: Int) {
        precondition(maxCount > 0)
        self.maxCount = maxCount
        _buffer = UnsafeMutablePointer<CChar>.allocate(capacity: maxCount)
        _buffer.initialize(repeating: 0, count: maxCount)
    }

    deinit {
        _buffer.deinitialize(count: maxCount)
        _buffer.deallocate()
    }

    public var count: Int { strlen(_buffer) }
    public var asString: String { String(cString: _buffer) }
}

This is our findLongest algorithm now:

	func findLongest() throws -> String? {
    var buffer = FileChunk(maxLength: maxLength)
    var longest: FileChunk? = nil
    var longestCount: Int = 0
    while let line: FileChunk = try readLine(reuseChunk: &buffer) {
        let count = line.count
        if count > longestCount {
            longest = line
            longestCount = count
        }
    }
    return longest.asString
}

Let’s profile it now:

The situation is looking much better. As we would expect, the majority of the time is spent on fgets and the associated C functions.

It is also interesting to notice that swift_retain and swift_release incur significant costs.

Optimization #4: making FileChunk non-copyable

To address this issue, we’ll convert FileChunk from a reference type to a non-copyable type:

	public struct FileChunk: ~Copyable {
    // the same implementation
}

But now we are required to explicitly manage the ownership of FileChunk as it can no longer be shared between multiple owners.

First, we need to adjust our readLine function to make ownership explicit:

	func readLine(toReuse: consuming FileChunk? = nil) throws -> FileChunk? {
    let currentChunk = toReuse ?? FileChunk(maxCount: maxLength)
    // The same implementation as previously
    return currentChunk
}

In this version, we mark the FileChunk parameter as consuming, meaning the function takes ownership of the argument. Once the buffer is filled, the function returns currentChunk, passing ownership back to the caller, allowing the buffer to be reused.

After making this change, we get two errors in our findLongest implementation:

	func findLongest() throws -> String? {
    var longest: FileChunk? = nil
    var buffer = FileChunk(maxCount: maxLength) ❌ // 'buffer' consumed in a loop
    var longestCount: Int = 0
    while let line: FileChunk = try readLine(toReuse: buffer) {
        let count = line.count
        if count > longestCount {
            longest = line ❌ // Implicit conversion to 'FileChunk?' is consuming
            longestCount = count
        }
    }
    return longest?.asString
}

Let’s solve them:

	func findLongest() throws -> String? {
    var longest: FileChunk? = nil
    var buffer = FileChunk(maxCount: maxLength)
    var longestCount: Int = 0
    while let line: FileChunk = try readLine(toReuse: buffer) {
        let count = line.count
        if count > longestCount {
            buffer = longest
            longest = consume line
            longestCount = count
        } else {
            buffer = consume line
        }
    }
    return longest?.asString
}

And now, let’s check the performance.

We can see that almost all of the time is spent on file reading operations, indicating a significant performance improvement.

Final results: performance gains

The performance improvements across the different implementations are summarized in the table below:

ImplementationTiming (ms)
NaiveDoesn’t complete in reasonable time
CChar array106295
Reuse allocation in CChar array100463
FileChunk3136
Non-copyable FileChunk2079

Note: If you are interested, the longest line can be found in this commit.

A hidden bug

If you look closely, you’ll notice a difference between our final implementation using a reference type and the one using a non-copyable type:

FileChunk as a reference type

	let count = line.count
if count > longestCount {
    longest = line
    longestCount = count
}

FileChunk as a non-copyable type

	let count = line.count
if count > longestCount {
    buffer = longest
    longest = consume line
    longestCount = count
} else {
    buffer = consume line
}

Upon closer inspection, we can see that the reference type implementation contains a hidden bug – it overrides the longest line found because the buffer is being reused. This issue is avoided with the non-copyable type, as it enforces unique ownership and prevents unintentional buffer reuse.

The value of non-copyable types

As we’ve seen, non-copyable types offer not only performance benefits but also improved safety compared to reference types. They are a safer and more ergonomic alternative to dealing with raw pointers.

One of the reasons the programming language C is known for its speed is that it exposes the full complexity of memory management, allowing direct manipulation through pointers. Swift, as a high-level programming language, abstracts much of this complexity. With the introduction of non-copyable types, Swift strikes a balance – allowing developers to write safe, high-performance code while maintaining the high-level programming experience.

For reference, the associated source code for this article can be found here.