Reputation: 1953
I'm doing loops on big arrays (images) and through Instruments I found out that the major bottleneck was Array.subscript.nativePinningMutableAddressor
, so I made this unit tests to compare,
// average: 0.461 seconds (iPhone6 iOS 10.2) ~5.8 times slower than native arrays
func testArrayPerformance() {
self.measure {
var array = [Float](repeating: 1, count: 2048 * 2048)
for i in 0..<array.count {
array[(i+1)%array.count] = Float(i)
}
}
}
// average: 0.079 seconds
func testNativeArrayPerformance() {
self.measure {
let count = 2048 * 2048
let array = UnsafeMutablePointer<Float>.allocate(capacity: count)
for i in 0..<count {
array[(i+1)%count] = Float(i)
}
array.deallocate(capacity: count)
}
}
As you can see, the native array is much faster. Is there any other way to access the array faster? "Unsafe" doesn't sound "safe", but what would you guys do in this situation? Is there any other type of array that wraps a native one?
For a more complex example, you can see follow the comments in this article: Rendering Text in Metal with Signed-Distance Fields I re-implemented that example in Swift, and the original implementation took 52 seconds to start up, https://github.com/endavid/VidEngine/tree/textprimitive-fail
After switching to native arrays, I went down to 10 seconds, https://github.com/endavid/VidEngine/tree/fontatlas-array-optimization
Tested on Xcode 8.3.3.
Edit1: The timings for this test are in Debug configuration, but the timings for the Signed Distance Fields example are in Release configuration. Thanks for the micro-optimizations (count, initialization) for the unit tests in the comments, but in the real world example those are negligible and the memory buffer solution is still 5 times faster on iOS.
Edit2: Here are the timings (Instruments session on iPhone6) of the most expensive functions in the Signed Distance Fields example,
Edit3: apart from performance issues, I had severe memory problems using Swift arrays. NSKeyedArchiver
would run out of memory and crash the app. I had to use the byte buffer instead, and store it in a NSData
. Ref. commit: https://github.com/endavid/VidEngine/commit/6c1822523a2b18759f294def3188755eaaf98b41
So I guess the answer to my question is: for big arrays of numeric data (e.g. images), better use memory buffers.
Upvotes: 2
Views: 1748
Reputation: 47896
As mentioned by Alexander, UnsafeMutablePointer
is not a native array, it's just a pointer operation.
Testing on iPhone 7+/iOS 10.3.2, in equivalent condition (both initialized) with Release build:
//0.030,0.027,0.017,0.027,0.024 -> avg 0.025
func testArrayPerformance2() {
self.measure {
let count = 2048 * 2048
var array = [Float](repeating: 1, count: count)
for i in 0..<count {
array[(i+1)%count] = Float(i)
}
}
}
//0.021,0.022,0.011,0.021,0.021 -> avg 0.0192
func testPointerOpPerformance2() {
self.measure {
let count = 2048 * 2048
let array = UnsafeMutablePointer<Float>.allocate(capacity: count)
array.initialize(to: 1, count: count)
for i in 0..<count {
array[(i+1)%count] = Float(i)
}
array.deinitialize(count: count)
array.deallocate(capacity: count)
}
}
Not a big difference. Less than 2 times. (About 1.3 times.)
Generally, Swift optimizer for Arrays work well for:
Whole Module Optimization would affect, but I have not tested.
If your more complex example takes 5 times to start up, it may be written in a hard-to-optimize manner. (Please pick up the core parts affecting the performance and include it in your question.)
Upvotes: 0
Reputation: 513
I decided to test the uninitialized Array performance on my 2014 Macbook Pro :
// average: 0.315 seconds (macOS Sierra 10.12.5)
func testInitializedArrayPerformance() {
self.measure {
var array = [Float](repeating: 1, count: 2048 * 2048)
for i in 0..<array.count {
array[(i+1)%array.count] = Float(i)
}
}
}
// average: 0.043 seconds (macOS Sierra 10.12.5)
func testUninitializedArrayPerformance() {
self.measure {
var array : [Float] = []
array.reserveCapacity(2048 * 2048)
array.append(0)
for i in 0..<(2048 * 2048) {
array.append(Float(i))
}
array[0] = Float(2048 * 2048-1)
}
}
// average: 0.077 seconds (macOS Sierra 10.12.5)
func testNativeArrayPerformance() {
self.measure {
let count = 2048 * 2048
let array = UnsafeMutablePointer<Float>.allocate(capacity: count)
for i in 0..<count {
array[(i+1)%count] = Float(i)
}
array.deallocate(capacity: count)
}
}
This confirms that the array initialization is causing a big performance hit.
Upvotes: 2
Reputation: 63271
Simply caching the count
improved the speed from 0.2s to 0.14s, which is twice the time it takes the pointer-based code. This is entirely expected, given that the array based code does a preinitialization of all elements to 1
.
Baseline:
After caching the count
:
Upvotes: 6