Skip to content

Instantly share code, notes, and snippets.

@Cananito
Last active August 29, 2015 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Cananito/f95bbfafdaa58da7e395 to your computer and use it in GitHub Desktop.
Save Cananito/f95bbfafdaa58da7e395 to your computer and use it in GitHub Desktop.
Draft for a ChaiOne engineering blog post: http://dev.chaione.com/keeping-performance-in-mind/

Keeping Performance in Mind

With Swift many people have been diving into the world of functional programming and value types. One of the many benefits of the former is the use of new abstractions in the way of functions: filter, map, and reduce.

I won’t talk about the pro’s and con’s of this approach, but for example, instead of writting custom loops:

  • If you want to convert an array of Ints to an array of Strings, you map: [1, 2, 3, 4].map({ String($0) }) // Produces ["1", "2", "3", "4"].
  • If you get the sum of all Ints of an array, you reduce: [1, 2, 3, 4].reduce(0, combine: { $0 + $1 }) // Produces 10.
  • If you want to know if an array doesn’t contain any object with a given flag, you filter and get the count. Or do you?

This last example came up in a recent project. With the excitement of this new paradigm I failed to see right away one issue: array.filter { $0.someFlag }.count isn’t a very performant thing to do.

The full condition looked something like this:

if (array.filter { $0.someFlag }.count == 0) {
	// Doesn’t contain what we want. Unlikely scenario.
} else {
	// Contains at least one of the things we want.
}

This internally creates a new array and loops through all of its contents. Our nice and convenient abstraction is potentially costly.

Before writting down the simple solution, I should explain that I tend to favor efficient code over easy-to-write-or-read code. I never do premature optimizations, but performance is always in the back of my head.

In fact, because the contents of the array aren’t that big, you could hardly tell that it wasn’t as fast as it should be. But why let this go when the change is simple enough?

The first thing that came to mind was: “I’ll go ahead and write a good ol’ for loop”. But wait! We don’t need to go back all the way. We don’t get to keep the established functional abstractions, but at least let’s do it functionally and efficient!

The function itself:

extension Array {
	func containsCondition(condition: T -> Bool) -> Bool {
		for element in self {
			if condition(element) {
				return true
			}
		}
		return false
	}
}

And now our if statement turns into:

if (array.containsCondition { $0.someFlag } == false) {
	// Doesn’t contain what we want. Unlikely scenario.
} else {
	// Contains at least one of the things we want.
}

Ignoring the naming of the function, we improved things a bit, while keeping the functional approach. A new array is no longer allocated, and we only loop until we find a given condition.

Using XCTestCase’s measureBlock, we can get an idea of the time it takes to run the code in each case:

class Object {
    let someFlag: Bool
    init(someFlag: Bool) {
        self.someFlag = someFlag
    }
}

class FunctionalPerformanceTests: XCTestCase {
    var array = Array<Object>()
    
    override func setUp() {
        super.setUp()
        
        // Populating the array with a similar ratio seen in the mentioned project.
        for (var i = 0; i < 200; i++) {
            self.array.append(Object(someFlag: false))
        }
        for (var i = 0; i < 2000; i++) {
            self.array.append(Object(someFlag: true))
        }
    }
    
    func testFilterCountPerformance() {
        self.measureBlock() {
            let condition = self.array.filter { $0.someFlag }.count == 0
        }
    }
    
    func testContainsConditionPerformance() {
        self.measureBlock() {
            let condition = self.array.containsCondition { $0.someFlag }
        }
    }
}

Your milage may vary, but the first approach took 0.016 seconds to run, while the second one took just 0.001 seconds (on a 2.8 GHz Intel Core i7, with 16 GB 1600 MHz DDR3 RAM). Pretty much what we were expecting.

In hindsight and like I mentioned before, the improvements are barely noticiable. You could only tell if the array consisted of thousands and thousands of elements. Or if accessing the object's flag was an expensive operation. Or if we had to do this repeatedly. But I still stand by the change.

Update (08/06/2015): At some point the contains function was added to the Swift standard library. So the way to do this now is:

if (array.contains { $0.someFlag } == false) {
	// Doesn’t contain what we want. Unlikely scenario.
} else {
	// Contains at least one of the things we want.
}
@mmorey
Copy link

mmorey commented Feb 18, 2015

Ignoring the naming of the function, we improved things a bit, while keeping the functional approach. A new array is no longer allocated, and we only loop until we find a given condition.

Give me proof that you actually improved it. Take a measurement with the old way using a measureBlock and one with the new way and compare the time.

@mmorey
Copy link

mmorey commented Feb 18, 2015

One of the many benefits of the former is the use of new abstractions: map, reduce, filter, sort, etc.

Do you need the "etc" here? How many more are there? Just list them all out. If I'm a beginner I'm going to start thinking about what other functions I don't know about instead of what this post is supposed to be about.

@mmorey
Copy link

mmorey commented Feb 18, 2015

If you want to convert an array of Ints to an array of Strings you map.
If you get the sum of all Ints of an array you reduce.
If you want to know if an array doesn’t contain any object with a given flag, you filter and get the count. Or do you?

This is great. Can you give a simple code example after each one?

@mmorey
Copy link

mmorey commented Feb 18, 2015

This internally creates a new array and loops through all of its contents. Our nice abstraction is costly.

Why does it create a new array? Or how do you know it creates a new array? Why is the abstraction costly? Is it because its taking up more memory? Or because its slower?

@Cananito
Copy link
Author

Re: https://gist.github.com/Cananito/f95bbfafdaa58da7e395#comment-1396129

I'm not sure what I want to do here. Based on talking a bit with Terry Lewis, the 3 tent poles are: filter, map, and reduce (aka fold), which come from the math world. But then there's traditional operations: sort, flat, etc. And then there's combinations: flatMap.

Building a list is probably overkill. I could drop sort, etc., which seems more in tune to what you prefer and what I think I'm leaning on.

@Cananito
Copy link
Author

Re: https://gist.github.com/Cananito/f95bbfafdaa58da7e395#comment-1396128

Added.

Re: https://gist.github.com/Cananito/f95bbfafdaa58da7e395#comment-1396131

Added.

Re: https://gist.github.com/Cananito/f95bbfafdaa58da7e395#comment-1396135

I'm not sure if I want to get into too much detail. What I am thinking about is adding potentially before costly. In reality, the more I think about it, the more this does look like a premature optimization hah!

@mmorey
Copy link

mmorey commented Feb 19, 2015

Your milage may vary, but the first approach took 0.016 seconds to run, while the second one took just 0.001 seconds. Pretty much what we were expecting.

What device did these numbers come from? Simulator? Actual device? A real device would be better. List the device and the amount of RAM and GHz it has.

@mmorey
Copy link

mmorey commented Feb 19, 2015

I'm not sure if I want to get into too much detail. What I am thinking about is adding potentially before costly. In reality, the more I think about it, the more this does look like a premature optimization hah!

You should close with something along those lines. It is perfectly fine to go through this whole exercise and then realize it wasn't really worth it. Add a concluding paragraph that wraps everything up and what the take aways are.

It is fine if the take away is that this isn't worth it.

@mmorey
Copy link

mmorey commented Feb 19, 2015

One of the many benefits of the former is the use of new abstractions in the way of functions: filter, map, reduce.

Should this be:
One of the many benefits of the former is the use of new abstractions in the way of functions: filter, map, and reduce.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment