4

I am trying to analyze a photo concurrently using a background thread from GCD. Here is the code I have written:

dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)) {
    for (var i = 0; i < 8; i++)
    {
        let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
        colorList.append(color)
    }
}

For clarification on the variable names, here are their descriptions:

photoAnalyzer is an instance of a class I wrote called Analyzer that holds all of the methods to process the image.

analyzeColors is a method inside the Analyzer class that does the majority of the analysis and returns a string with the dominant color of the passed in image

imageStrips is an array of UIImage's that make up the portions of the original image

colorList is an array of strings that stores the return values of the analyzeColor method for each portion of the image.

The above code runs sequentially since the for loop only accesses one image from the imageList at a time. What I am trying to do is analyze each image in imageStrips concurrently, but I had no idea how to go about doing that.

Any suggestions would be greatly appreciated. And if you would like to see all of the code to further help me I can post a GitHub link to it.

EDIT This is my updated code to handle 8 processors concurrently.

dispatch_apply(8, imageQueue) { numStrips -> Void in
    let color = self.photoAnalyzer.analyzeColors(imageStrips[numStrips])
    colorList.append(color)
}

However, if I try to use more than 8 the code actually runs slower than it does sequentially.

steveclark
  • 537
  • 9
  • 27
  • I should also mention that I am extremely new to multithreading on iOS. – steveclark Apr 27 '15 at 03:53
  • There's answers in this thread already that will answer your question: http://stackoverflow.com/questions/24170706/whats-the-best-way-to-set-up-concurrent-execution-of-for-loops-in-objective-c You've set that up to use a concurrent queue, however `dispatch_apply` is what you're going to need to take advantage of concurrent processing. – John Rogers Apr 27 '15 at 03:55
  • @JohnRogers That seemed to work at least a little bit. I tried it for an image split in to 20 pieces, but it only analyzed 8 at a time. Is that restriction because of the number of processors available? – steveclark Apr 27 '15 at 04:26
  • You're 100% correct! It will perform one operation per thread, in your case 8. – John Rogers Apr 27 '15 at 04:27
  • Okay, so I am limited to 8 processors? The reason I ask is because an analysis of a 400x400 pixel image took 75 seconds sequentially and 66 seconds on the 8 processors. I was hoping to be able to split the image up into 16, 32, 48, etc. strips and plot the time increase. – steveclark Apr 27 '15 at 04:39
  • `dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply([images count], queue, ^(size_t index){ // do something });` – John Rogers Apr 27 '15 at 04:46
  • I don't doubt that that would work in Objective-C, but I am having trouble translating it to Swift. I have modified my post with my current code. – steveclark Apr 27 '15 at 04:54
  • @Rob's written an excellent answer below for you. – John Rogers Apr 27 '15 at 04:56

2 Answers2

4

There are a couple of ways of doing this, but there are a couple of observations before we get to that:

  • To try to maximize performance, if you do any concurrent processing, be aware that you are not guaranteed the order in which they will complete. Thus a simple colorList.append(color) pattern won't work if the order that they appear is important. You can either prepopulate a colorList and then have each iteration simply do colorList[i] = color or you could use a dictionary. (Obviously, if order is not important, then this is not critical.)

  • Because these iterations will be running concurrently, you'll need to synchronize your updating of colorList. So do your expensive analyzeColors concurrently on background queue, but use a serial queue for the updating of colorList, to ensure you don't have multiple updates stepping over each other.

  • When doing concurrent processing, there are points of diminishing returns. For example, taking a complex task and breaking it into 2-4 concurrent loops might yield some performance benefit, but if you start increasing the number of concurrent threads too much, you'll find that the overhead of these threads starts to adversely affect performance. So benchmark this with different degrees of concurrency and don't assume that "more threads" is always better.

In terms of how to achieve this, there are two basic techniques:

  1. If you see Performing Loop Iterations Concurrently in the Concurrency Programming Guide: Dispatch Queues guide, they talk about dispatch_apply, which is designed precisely for this purpose, to run for loops concurrently.

    colorList = [Int](count: 8, repeatedValue: 0)  // I don't know what type this `colorList` array is, so initialize this with whatever type makes sense for your app
    
    let queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
    
    let qos_attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0)
    let syncQueue = dispatch_queue_create("com.domain.app.sync", qos_attr)
    
    dispatch_apply(8, queue) { iteration in
        let color = self.photoAnalyzer.analyzeColors(imageStrips[iteration])
        dispatch_sync(syncQueue) {
            colorList[iteration] = color
            return
        }
    }
    
    // you can use `colorList` here
    

    Note, while these iterations run concurrently, the whole dispatch_apply loop runs synchronously with respect to the queue from which you initiated it. This means that you will not want to call the above code from the main thread (we never want to block the main thread). So will likely want to dispatch this whole thing to some background queue.

    By the way, dispatch_apply is discussed in WWDC 2011 video Blocks and Grand Central Dispatch in Practice.

  2. Another common pattern is to create a dispatch group, dispatch the tasks to a concurrent queue using that group, and specify a dispatch_group_notify to specify what you want to do when it's done.

    colorList = [Int](count: 8, repeatedValue: 0)  // I don't know what type this `colorList` array is, so initialize this with whatever type makes sense for your app
    
    let group = dispatch_group_create()
    let queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
    
    let qos_attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0)
    let syncQueue = dispatch_queue_create("com.domain.app.sync", qos_attr)
    
    for i in 0 ..< 8 {
        dispatch_group_async(group, queue) {
            let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
            dispatch_sync(syncQueue) {
                colorList[i] = color
                return
            }
        }
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue()) {
        // use `colorList` here
    }
    
    // but not here (because the above code is running asynchronously)
    

    This approach avoids blocking the main thread altogether, though you have to be careful to not add too many concurrent dispatched tasks (as the worker threads are a very limited resource).

In both of these examples, I created a dedicated serial queue for synchronizing the updates to colorList. That may be overkill. If you're not blocking the main queue (which you shouldn't do anyway), you could dispatch this synchronization code to the main queue (which is a serial queue). But it's probably more precise to have a dedicated serial queue for this purpose. And if this was something that I was going to be interacting with from multiple threads constantly, I'd use a reader-writer pattern. But this is probably good enough for this situation.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Wow, very thorough answer thank you. All the app does is allow a user to select a photo, press an "Analyze" button and when the photo is finished analyzing an Alert appears telling the user what the dominant color is. From my understanding, keeping the main queue clear is to avoid forcing the user to sit and wait while operations are performing, but since this app literally just performs one task I don't think doing what you did would be overkill. – steveclark Apr 27 '15 at 05:00
  • Also, the `colorList` just stores a string for the dominant color of that particular sub-image. So as long as the sub-image is processed correctly (which my method does) then all I have to do is count the number of times each color appears in the `colorList` and thats the dominant color of the whole image. – steveclark Apr 27 '15 at 05:03
  • I was guessing you were trying to do something like that, but had no way of knowing what type `colorList` was. Clearly, you can just replace my `[Int](count: 8, repeatedValue: 0)` with `[String](count: 8, repeatedValue: "")` (or whatever, and the rest is pretty much the same). – Rob Apr 27 '15 at 05:11
  • As you mentioned, there is a point of diminishing return. I am analyzing a 400x400 pixel photo and it takes over a minute to do so. Obviously I cannot just keep splitting the photo up and increasing the loop (once again, as you mentioned) but is there any other way to increase the speed at which this photo is analyzed? My only thought was to analyze every other pixel. – steveclark Apr 27 '15 at 05:35
  • A 400x400 image is not extraordinarily large, so 1 min sounds like a lot. Instruments' Time Profiler might help you diagnose what the source of the issue is. See WWDC 2012 video [Designing Concurrent User Interfaces](https://developer.apple.com/videos/wwdc/2012/?id=211). It's a bit dated, but the basic techniques still apply. I wonder if the vImage routines might be helpful, too. See the [Histogram](https://developer.apple.com/library/ios/documentation/Performance/Conceptual/vImage/HistogramOperations/HistogramOperations.html#//apple_ref/doc/uid/TP30001001-CH207-SW1) discussion. – Rob Apr 27 '15 at 05:50
  • BTW, Swift experiences larger performance hits than Objective-C when doing debug builds, so make sure to test this with release build and on actual device. It will make a difference. – Rob Apr 27 '15 at 05:52
  • I was also very curious as to why it took so long for an image of that size to take so long to process. The only thing I could come up with is that my code is just inefficient. I still have a decrease in time when analyzing a photo concurrently versus sequentially which is enough for me for what I am doing. However, I'm not asking you or anyone who might read this to analyze my code, but if you wanted to take a look or clone into it and fool around with it the source code is here: https://github.com/steveclark91/Dominant_Color – steveclark Apr 27 '15 at 06:09
  • Glancing at your code, the `getPixelColor` function is copying the data provider's data. That means that you're creating _another_ copy of the `NSData` for _every_ pixel. That's inefficient. The Time Profiler tool might help you find these sorts of issues in the future. – Rob Apr 27 '15 at 14:39
  • Superb answer, thanks Rob! When creating your syncQueue, you could eliminate manually creating the qos_attr and just go with `let syncQueue = dispatch_queue_create("com.domain.app.sync", DISPATCH_QUEUE_SERIAL)`. If, however, you want to create the qos_attr param manually, the docs state that the 3rd param (relative_priority) should be a negative offset, "This value must be less than 0 and greater than MIN_QOS_CLASS_PRIORITY" http://bit.ly/dispatch_queue_attr_make_with_qos_class – Scott Gardner May 10 '15 at 16:46
  • Lol. The value of zero was taken directly from [WWDC 2014 QoS presentation](https://developer.apple.com/videos/wwdc/2014/?id=716). Also, the header comments say "Passing a value greater than zero or less than `QOS_MIN_RELATIVE_PRIORITY` results in `NULL` being returned." That suggests that if you want to reduce priority that you should use negative value, but that zero is acceptable. It would seem strange to say you cannot use QoS with one's own queues unless you reduce the relative priority. But I agree that that is the logical inference of the documentation (but not the header comments). – Rob May 10 '15 at 17:13
  • @Rob, again thank you for your answer. It helped tremendously and I was able to ace the project. However, now that my semester is over and have some time on my hands I wanted to make this app (extremely) more efficient. You mentioned how expensive it is to pass a copy of the data every time, so that is where I am going to start. My first thought is to pass the reference to the image instead of the image itself, but do you have any other recommendations for enhancing? – steveclark May 14 '15 at 01:24
  • Just flip the logic around. Remove the "retrieve data provider's data" logic from `getPixelColor`, but instead make that data buffer a parameter you pass _to_ `getPixelColor`. Then you can get the data provider's data once, before you start your loop, and then the call to `getPixelColor` will be _much_ faster. – Rob May 14 '15 at 01:42
-1
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)) {
    for (var i = 0; i < 8; i++)
    {
 dispatch_async(dispatch_get_main_queue(), ^(){
    //Add method, task you want perform on mainQueue
    //Control UIView, IBOutlet all here
        let color = self.photoAnalyzer.analyzeColors(imageStrips[i])
        colorList.append(color)
    });
    }
}
Binladenit Tran
  • 121
  • 1
  • 1
  • 7