Single file versus multiple files in a Swift package

I have a Swift package that does various numerical calculations. All of the algebra related code is in an Algebra.swift file which contains a protocol and extensions that conform to the protocol. This file has gotten very large and tedious to work with. So I extracted the code into several individual files such as the following:

.
├── Algebra.swift
├── Double+Algebra.swift
├── Float+Algebra.swift
├── Int+Algebra.swift
├── Matrix+Algebra.swift
└── Vector+Algebra.swift

where the protocol is defined in Algebra.swift and all the conforming type extensions are in the + files. I find this easier to work with instead of one large file but it drastically increases the number of files in the package.

I looked at some other Swift packages and noticed BitArray in swift-collections uses multiple files instead of one large BitArray.swift file. However, Chunked in swift-algorithms puts everything in one file including the extensions. Using one file versus multiple files seems to vary by project but I'm not sure why one approach is used over the other.

Does it matter in Swift if everything is in one large file compared to several smaller files? Does it affect compilation speed or runtime performance? What are the pros and cons of one file versus multiple files?

Depends on how many smaller files. You'll potentially get multiple swift-frontend invocation for each file, but dependencies between files make parallelized builds not entirely straightforward. They at least can be parsed independently in parallel I suppose, but I don't think parsing was ever a bottleneck for most projects.

I don't have enough knowledge of swiftc and swift-frontend to clarify more from that perspective, but I'll comment from the current SwiftPM and build system implementation perspective: SwiftPM traverses all files in all targets in the root package and all of its transitive dependencies, and then serializes them in a (potentially huge) build manifest file before handing that off to a build system. The actual build with swiftc/clang invocations can't start before traversal and serialization of the build manifest is finished. The new build system changed the serialization format, but this principle stays the same: any addition of packages, targets, and files in them increases the time spent in this traversal and serialization step, which slows down the overall build time. And that's also the fixed cost incurred for any incremental build, not just cold builds.

If you split 1 file into 5 files, you probably won't notice the build manifest serialization slowdown. But if you split 100 files into 500 files, that could add significant delay to swift build invocations. That delay is also added to all packages that depend on your package with those 500 files, because their build manifests will have at least 500 build graph nodes (or more, depending on some implementation details) added to them.

4 Likes

Sounds like it's better to just keep everything in one file and navigate through that file with my text editor. That would definitely clean up the package directory.

Performance concerns aside, one concrete benefit of breaking your code up into multiple files is that private and fileprivate declarations are inaccessible from other files. Especially for risky code, keeping the amount of code you have to read to verify correct behavior to a minimum can be handy.

6 Likes

Putting all your eggs in one basket - not a good idea. :slight_smile:

I just wrote a blog post on this topic from an architectural perspective—without diving into build-time technicalities.

It might be useful if you’re interested in how to build a package step-by-step, starting simple and gradually adding complexity while surfacing the issues that come with it.

Read it here :backhand_index_pointing_right: coenttb.com/blog/4

Your article is great. Thank you for sharing. I have tried the multiple targets approach but it doesn't play nice with DocC. It gets confused sometimes when linking to code with similar names that are in different targets. I also had issues with the rendered docs on Swift Package Index where links wouldn't work. It would be great if you could write a follow-up article about the different architectural approaches and how they affect documentation and testing.

1 Like

Those are excellent suggestions - thank you! I hadn’t thought about writing on DocC, mostly because I ran into similar cross-linking limitations (though I know improvements are underway).

Testing, on the other hand, has been a real pleasure in a multi-target or multi-package setup. With each target narrowly scoped, achieving thorough test coverage becomes very manageable. If you’re curious how I approach this, feel free to check out swift-html-css-pointfree.

If you enjoyed the blog, I’d love it if you subscribed to my newsletter - it really helps!

1 Like

Another issue with multi-module packages is that they can't be delivered as a binary dependency as XCFrameworks can only contain one module. If SPM supported regular frameworks then it wouldn't be an issue.

Overall though breaking things into modules fits well with how Swift compiles and with the access controls in Swift. With packages it let us drop Carthage and deliver our mixed language Swift/Obj-C code as a single package as well.

I took your suggestion and wrote about testing. Would love to hear what you think!

:backhand_index_pointing_right: Click here to read the article