As an exercise I\'ve rewritten a few of Swift\'s higher order functions, one being .filter
. I decided to measure my version of .filter
against Swif
Ok, so after reading all posted comments, I decided to also benchmark, and here are my results. Oddly enough, the built-in filter
seems to perform worse than a custom implementation.
TL;DR; Because your function is short, and the compiler has access to it source code, the compiler inlines the function call, which enables more optimisations.
Another consideration is as your myFilter
declaration doesn't take into consideration exception throwing closures, thing that the built-in filter
does.
Add @inline(never)
, throws
and rethrows
to your myFilter
declaration and you'll get similar results as for the built-in filter
I used mach_absolute_time()
to obtain accurate times. I didn't converted the results to seconds as I was merely interested in comparison. Tests were run on Yosemite 10.10.5 with Xcode 7.2.
import Darwin
extension Array {
func myFilter(@noescape predicate: Element -> Bool) -> [Element] {
var filteredArray = [Element]()
for x in self where predicate(x) {
filteredArray.append(x)
}
return filteredArray
}
}
let arr = [Int](1...1000000)
var start = mach_absolute_time()
let _ = arr.filter{ $0 % 2 == 0}
var end = mach_absolute_time()
print("filter: \(end-start)")
start = mach_absolute_time()
let _ = arr.myFilter{ $0 % 2 == 0}
end = mach_absolute_time()
print("myFilter: \(end-start)")
In debug
mode, filter
is faster than myFilter
:
filter: 370930078
myFilter: 479532958
In release
, however, myFilter
is much better than filter
:
filter: 15966626
myFilter: 4013645
What's even more strange is that an exact copy of the built-in filter
(taken from Marc's comment) behaves better than the built-in one.
extension Array {
func originalFilter(
@noescape includeElement: (Generator.Element) throws -> Bool
) rethrows -> [Generator.Element] {
var result = ContiguousArray<Generator.Element>()
var generator = generate()
while let element = generator.next() {
if try includeElement(element) {
result.append(element)
}
}
return Array(result)
}
}
start = mach_absolute_time()
let _ = arr.originalFilter{ $0 % 2 == 0}
end = mach_absolute_time()
print("originalFilter: \(end-start)")
With the above code, my benchmark app gives the following output:
filter: 13255199
myFilter: 3285821
originalFilter: 3309898
Back to debug
mode, the 3 flavours of filter
give this output:
filter: 343038057
myFilter: 429109866
originalFilter: 345482809
filter
and originalFilter
give very close results. Which makes me think that Xcode is linking against the debug version of Swifts stdlib. However when build in release
, Swifts stdlib performs 3 times better than in debug
, and this confused me.
So the next step was profiling. I hit Cmd+I
, set the sample interval to 40us, and profiled the app two times: one when only the filter
call was enabled, and one with myFilter
enabled. I removed the printing code in order to have a stack-trace as clean as possible.
Built-in filter
profiling:
(source: cristik-test.info)
myFilter
:
Eureka!, I found the answer. There's no track of the myFilter
call, meaning that the compiler inlined the function call, thus enabling extra optimizations that give a performance boost.
I added the @inline(never)
attribute to myFilter
, and it's performance degraded.
Next, to make it closer to the built-in filter was to add the throws
and rethrows
declaration, as the built-in filter allows passing closures that throw exceptions.
And surprise (or not), this is what I got:
filter: 11489238
myFilter: 6923719
myFilter not inlined: 9275967
my filter not inlined, with throws: 11956755
Final conclusion: the fact that the compiler can inline the function call, combined with lack of support for exceptions was responsible for the better performance of your custom filtering method.
The following code gives results very similar to the build-in filter
:
extension Array {
@inline(never)
func myFilter(predicate: Element throws -> Bool) rethrows -> [Element] {
var filteredArray = [Element]()
for x in self where try predicate(x) {
filteredArray.append(x)
}
return filteredArray
}
}
Swift's filter
should perform better, because:
#1 might not give much difference, as function calls are not very expensive
#2 on the other hand might make a big difference for large arrays. Appending a new element to the array might result in the array needing to increase its capacity, which implies allocating new memory and copying the contents of the current state.