iTranslated by AI
[Swift] Do you really need that array addition? A recommendation for chain()
In Swift, we often want to process two arrays by joining them. For example, in the following case:
let array1 = getArrayFromSource1()
let array2 = getArrayFromSource2()
for item in array1 {
doSomething(with: item)
}
for item in array2 {
doSomething(with: item)
}
In such cases, writing a loop for array1 + array2 makes the code cleaner.
// Clean code!
for item in array1 + array2 {
doSomething(with: item)
}
While this looks better, there is actually a problem.
Adding Arrays is Expensive
Creating an array is an expensive operation. This is because memory must be allocated for the number of elements and then written to. Therefore, adding arrays—especially large ones—becomes a very heavy operation.
Consequently, adding arrays as shown above can result in slower performance compared to simply writing two loops.
// Actually, writing it this way is faster
for item in array1 {
doSomething(with: item)
}
for item in array2 {
doSomething(with: item)
}
Adding them just for the sake of clean code isn't ideal, as it needlessly generates heat (wastes processing power). So, what is the best approach in these situations?
chain()
In general, what a for loop requires is conformance to Sequence. While Sequence requires an iterator implementation, there is inherently no need to recreate the array.
If we could "first execute the iterator for array1 and then execute the iterator for array2 once it reaches nil," adding the arrays would be unnecessary.
This is exactly what chain() from Swift Algorithms does.
import Algorithms
for item in chain(array1, array2) {
doSomething(with: item)
}
chain combines two sequences, S1: Sequence and S2: Sequence, and returns a value of type Chain2Sequence<S1, S2>. Since Chain2Sequence<S1, S2> conforms to Sequence, it can be used in a for loop as shown above.
In terms of mental model, if zip connects sequences in parallel, chain connects them in series.
Since chain does not allocate extra memory, the loop can be executed quickly.
Introducing chain to your project
chain is not included in the Swift standard library; you need to import and use Swift Algorithms, which is an open-source library by Apple.
The installation process is the same as any general Swift Package, but if you would like to know the detailed steps, this article might be a good reference.
Measuring the performance of chain
To measure the performance of chain, I've prepared a package containing simple tests. It's just a clone and one command away, so please feel free to give it a try.
Here, we compare the performance of adding relatively small arrays versus combining them using chain.
// When adding two arrays
func testTake2ArrayAdditionPerformance() throws {
measure {
let a = Array(0 ..< 100)
let b = Array(100 ..< 200)
for _ in 0..<1000000 {
takeSequence(a + b)
}
}
}
// When combining two arrays using chain
func testTake2ArrayAdditionPerformance() throws {
measure {
let a = Array(0 ..< 100)
let b = Array(100 ..< 200)
for _ in 0..<1000000 {
takeSequence(chain(a + b))
}
}
}
The implementation of takeSequence is as follows, where it simply iterates through the loop and performs a minor operation.
func takeSequence(_ sequence: some Sequence<Int>) {
for i in sequence {
_ = i + 1
}
}
First, here are the results for combining two arrays:
* testTake2ArrayAdditionPerformance
measured [Time, seconds] average: 0.227, relative standard deviation: 3.636%, values: [
0.232912,
0.233811,
0.224064,
0.219582,
0.211700,
0.234103,
0.215850,
0.237818,
0.230090,
0.226787
]
* testTake2ArrayChainPerformance
measured [Time, seconds] average: 0.0000233, relative standard deviation: 94.828%, values: [
0.000089,
0.000023,
0.000016,
0.000015,
0.000015,
0.000015,
0.000015,
0.000015,
0.000015,
0.000015
]
It was approximately 9,700 times faster. This is even more effective than I expected.
So, how does the performance of chain compare to simply running multiple loops sequentially? Let's try the following test.
func testTake2ArrayPerformance() throws {
measure {
let a = Array(0 ..< 100)
let b = Array(100 ..< 200)
for _ in 0..<1000000 {
takeSequence(a)
takeSequence(b)
}
}
}
The results were as follows:
* testTake2ArrayPerformance
measured [Time, seconds] average: 0.0000262, relative standard deviation: 94.323%, values: [
0.000097,
0.000022,
0.000016,
0.000015,
0.000015,
0.000039,
0.000016,
0.000014,
0.000014,
0.000014
]
Surprisingly, it is almost the same as when using chain. This shows that by using chain, you can achieve high performance without sacrificing code simplicity.
Next, here is the case for combining three arrays. Since Chain3Sequence does not exist, the resulting type is something like Chain2Sequence<Chain2Sequence<A, B>, C>.
* testTake3ArrayAdditionPerformance
measured [Time, seconds] average: 0.574, relative standard deviation: 6.311%, values: [
0.571852,
0.609838,
0.535366,
0.533511,
0.517794,
0.587197,
0.644619,
0.573711,
0.593577,
0.573296
]
* testTake3ArrayChainPerformance
measured [Time, seconds] average: 0.0000257, relative standard deviation: 77.464%, values: [
0.000085,
0.000024,
0.000020,
0.000019,
0.000019,
0.000018,
0.000018,
0.000018,
0.000018,
0.000018
]
This is also about 22,000 times faster. It works like a charm.
However, the performance of chain actually drops significantly when combining four or more arrays.
* testTake4ArrayAdditionPerformance
measured [Time, seconds] average: 0.919, relative standard deviation: 6.813%, values: [
0.890148,
0.869612,
0.909080,
0.890426,
0.892907,
1.090301,
0.968163,
0.916662,
0.882951,
0.882945
]
* testTake4ArrayChainPerformance_b
measured [Time, seconds] average: 0.276, relative standard deviation: 4.387%, values: [
0.286580,
0.273148,
0.266839,
0.295965,
0.270032,
0.276245,
0.264713,
0.295149,
0.265702,
0.261002
]
In this case, the performance improvement is only 3.3x. While this is still a decent gain, it is a somewhat curious result considering that the values for up to three arrays were almost zero. In fact, since simply writing four loops sequentially would result in a value close to zero, it suggests that chain struggles with combining four or more arrays.
While it doesn't quite fall behind array addition, the performance gain drops to about 2x when dealing with the sum of five arrays.
Summary
In this article, I recommended using chain() instead of adding arrays together. For two or three arrays, performance improves dramatically. Although the benefits decrease when using four or more arrays, the performance is still significantly improved. Of course, in typical use cases, the performance gain seen with two or three arrays is already quite sufficient.
It’s so convenient that I wish it were in the standard library, so please give it a try.
Discussion