9 Ways to Boost Your Swift Code Performance
Some tips on squeezing Swift for speed
Swift is a fast language and it gets faster in every release. iOS devices are also fast, so the chances are these tips won’t be needed at all. Even in those cases where we do bump into performance issues, it’s best practice to bypass them using caching, background threads or any other technique.
But when dealing with big numbers, slow devices or even just for theory’s sake, knowing these tips can’t harm you.
Set Optimization Level in Build Settings
The first thing you need to do to optimize your code is to let the Xcode optimize it by itself. The Xcode compiler is smart enough to know when it can ignore functions results that have no use, or call directly to methods that weren’t subclassed, but this can come with the cost of additional compile-time or size.
Go to build settings, and locate the “Swift Compiler — Code Generation” section. There you can find the “Optimization Level” Settings where you have three options:
- No Optimization
- Optimization for Speed
- Optimization for Size
It is usually best to set “Optimization for Speed” for release builds, and “Optimization for Size” for debug, but of course it depends on your project’s needs.
Basically, if you need power in favor of size, set optimization level to “Optimize for Speed”.
Use “Final” and “Private” for Methods and Classes
One of the things Swift Optimization Level handles is a direct or indirect call to methods. As you probably know, Swift is an Object-Oriented language, meaning you can subclass, and override methods to extend functionality. To make it work, Swift uses something called Dynamic Dispatch.
Dynamic Dispatch is the algorithm that decides which method should be invoked whenever a message is sent to an object. It uses a “vTable” (Virtual Table).
Whenever an object is requested to return the method address, it searches the virtual table for its address. This is not an obvious action — remember, it needs to lookup for a subclass and see if this method is overridden and return this method instead. This method can also be overridden by another subclass, so it needs to do it again and again until it gets to the bottom of it. To help Swift optimize this expensive task, you can add the attribute “final” to the start of the method, variable, or even the whole class.
When you choose to optimize your swift code in build settings, the “final” attribute will ensure the call to the method is direct, without looking for subclass implementation.
Avoid “Print” in Release Builds
Printing to console is great for debugging, but developers sometimes have a bad habit of leaving print commands floating around in the code. Since print commands are written to the console, and that makes use of the disk, it seems to be a very expensive I/O action. How expensive? Very. Take a look:
The test with the “print” call ran 1600 times slower!
My advice is to never call directly to “print”, but to wrap it with some class, and inside that class add a MACRO so you’ll never forget those “print” commands ever again!
“Inline” Your Code
It’s always best practice to create small functions, with each function dedicated to a single task. But separating your code into small functions also comes with a performance cost— you add another function to the stack, and you’re forcing Swift to use the virtual table to dynamically dispatch your call. When you want to squeeze your code for more performance, you can give up your nice code, and “inline” your method so it will be faster. Again, it’s a trade-off between clean code and performance.
Dealing With Arrays
A lot of performance optimizations are mainly relevant when dealing with a lot of iterations, or in other words, arrays.
If you know how an array is built and you’re aware of the trade-offs, you can make additional optimizations for your code.
Here are some examples for optimizing code with arrays:
Use “withUnsafeBufferPointer” when iterating array
When dealing with Swift objects in general, we sometimes forget about memory issues or safety because Swift is handling everything for us. This comes at a cost for performance. If you want to trade safety for performance, you can use the “withUnsafeBufferPointer” method to get the array of pointers for the array elements.
But you should be careful — if for some reason those elements are deallocated you may have a crash when approaching it in your code.
This is a less familiar array type, but it comes in handy when you know how to take advantage of it.
In general, Arrays keep their objects in memory blocks — not necessarily contiguous. This means that whenever you append a new item to the array, it just finds a free block, allocates it and adds it to the array. This is great for appending performance, but it’s less efficient for iterating. So, if this is a large array you know you’re going to be iterating, ContiguousArray can be a solution.
ContiguousArray makes sure all items in the array are contiguous with each other. This is very helpful in finding the next element. But, as usual, we are dealing with a tradeoff — there’s no magic here. Using ContiguousArray practically, saying that we are adding constraints for the array management, and simple actions such as inserting or appending can be heavier now. We are now forcing all its elements to be contiguous, so it depends on your use case.
Use Values (Structs) and Not References (Classes) in Array
This is another great tip. You probably know that arrays can bridge to NSArray. NSArray can hold only objects, and array can hold both objects (references) and values.
When an array in Swift holds references, it automatically gets the properties of the NSArray, and therefore cannot be optimized. But, if you only keep values such as Int or Structs in the array, it can be easily optimized by the compiler.
If you have to choose between a struct and a class, this is a big advantage of structs — holding structs in an array is much more efficient than holding classes.
In the above example, the action of filling the structs array was four times faster!
Use a Linked List Instead of an Array
Arrays are simple to use, but in some cases linked lists can be more efficient. Since linked lists are just items that point to each other, inserting an item in the middle of the list is very easy — just connect the pointers properly and you’re there. In arrays, it can be more expensive. So, whenever you need to replace item locations, appending, etc, linked lists are very quick. On the other hand, Arrays outperform linked lists when you have to sort or do a lookup.
Limit Protocols to Class Only if Possible.
If you know that the protocol you define is only for classes, mark it as class protocol. When the compiler knows the protocol is only for classes, it can optimize the ARC (Automatic Reference Counting) making your code run faster.
The Tradeoff Table (Or “Optimization Price List”)
Nothing is free, especially speed improvements, so here I have summarized the tips above with the price you need to pay when you implement them in your project:
- Optimization Level in Build Settings: payment: build size
- Use “final” and “private” for methods and classes — payment: constraints with subclass and dispatching
- Avoid “print” in release builds — payment: no logs to console
- “Inline” Your Code — Payment: duplicate code; code is not “clean”
- Use “withUnsafeBufferPointer” when iterate array — Payment: safety
- ContiguousArray — Payment: appending can be slow
- Use Values (Structs) and not References (Classes) in Array — Payment: depends on your structs — can be heap memory
- Use a Linked List instead of Array — Payment: collection classing methods can be slow, such as sorting or lookups
- Limit Protocols to Class Only if possible. — Payment: as the title says — you have constraints here.
As I said at the beginning, many performance issues can be solved using different app architecture techniques, caching, or any other algorithm method.
But it’s always good to know how to squeeze your Swift engine for maximum speed.