iOS并发 – 未达到理论上的最大值

我是Grand Central Dispatch的新手,并且已经运行了一些测试,并对图像进行了一些处理。 基本上我是按顺序运行灰度算法并使用GCD并比较结果。

这是基本循环:

UInt8 r,g,b; uint pixelIndex; for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { pixelIndex = (uint)(y * width + x); if (pixelIndex+2  MAX_COLOR_VALUE) { value = MAX_COLOR_VALUE; } targetData[pixelIndex] = value; self.imageData[pixelIndex] = value; } } } 

它只是贯穿并获取红色,绿色和蓝色的平均值,并将其用于灰度值。 很简单。 现在,并行版本基本上将图像分成几部分,然后单独计算这些部分。 即2,4,8,16和32部分。 我正在使用基本的GCD,所以传递每个部分,因为它自己的块同时运行。 这是GCD包装的代码:

 dispatch_group_t myTasks = dispatch_group_create(); for (int startX = 0; startX < width; startX += width/self.numHorizontalSegments) { for (int startY = 0; startY < height; startY += height/self.numVerticalSegments) { // For each segment, enqueue a block of code to compute it. dispatch_group_async(myTasks, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // grayscale code... }); } } dispatch_group_wait(myTasks, DISPATCH_TIME_FOREVER); 

一切都很好。 但我不理解的是加速/ CPU使用率。 在模拟器中运行测试(使用我的双核CPU)我得到:

  • ~0.0945s顺序运行时间
  • 〜0.0675s使用GCD运行时间

这是大约28%的加速(也就是顺序版本的时间占72%)。 从理论上讲,在2核机器上100%加速是最大的。 所以这远远不够,我无法弄清楚为什么。

我监控CPU使用率并且最大值大约为118% – 为什么它没有达到接近200%? 如果有人知道我应该改变什么,或者什么是罪魁祸首,我将非常感激。

我的理论:

  • 没有足够的CPU工作(但图像约为3,150,000像素)
  • 没有足够的时间来接近200%? 也许每个线程在开始咀嚼大部分CPU之前需要更长的运行时间?
  • 我想也许开销很高,但是测试将32个空块发送到队列(也在一个组中)大约需要大约0.0005秒。

最有可能的猜测。 在单线程情况下,您受CPU限制。 在multithreading情况下,您受内存限制。 换句话说,两个内核正在以最大总线带宽从DRAM读取数据。 结果,核心最终空转等待更多数据处理。

您可以通过进行真正的亮度计算来测试我的理论:

 int value = floor( 0.299 * red + 0.587 * green + 0.114 * blue ); 

在给定8位rgb值的情况下,该计算将产生0到255范围内的灰度值。 它还为处理器提供了更多的每像素工作量。 如果更改该行代码,单线程案例的时间应该会有所增加。 而且,如果我是正确的,那么multithreading案例应该表现出更好的性能提升,占单线程时间的百分比。


我决定在模拟器和iPad2上运行我自己的一些基准测试。 我的代码结构如下。

单螺纹

 start = TimeStamp(); for ( y = 0; y < 2048; y++ ) for ( x = 0; x < 1536; x++ ) computePixel(); end = TimeStamp(); NSLog( @"single = %8.3lf msec", (end - start) * 1e3 ); 

两个线程使用GCD

 dispatch_group_t tasks = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ); start = TimeStamp(); dispatch_group_async( tasks, queue, ^{ topStart = TimeStamp(); for ( y = 0; y < 1024; y++ ) for ( x = 0; x < 1536; x++ ) computePixel(); topEnd = TimeStamp(); }); dispatch_group_async( tasks, queue, ^{ bottomStart = TimeStamp(); for ( y = 1024; y < 2048; y++ ) for ( x = 0; x < 1536; x++ ) computePixel(); bottomEnd = TimeStamp(); }); wait = TimeStamp(); dispatch_group_wait( tasks, DISPATCH_TIME_FOREVER ); end = TimeStamp(); NSLog( @"wait = %8.3lf msec", (wait - start) * 1e3 ); NSLog( @"topStart = %8.3lf msec", (topStart - start) * 1e3 ); NSLog( @"bottomStart = %8.3lf msec", (bottomStart - start) * 1e3 ); NSLog( @" " ); NSLog( @"topTime = %8.3lf msec", (topEnd - topStart) * 1e3 ); NSLog( @"bottomeTime = %8.3lf msec", (bottomEnd - bottomStart) * 1e3 ); NSLog( @"overallTime = %8.3lf msec", (end - start) * 1e3 ); 

这是我的结果。

在模拟器上运行(r + g + b)/ 3

 2014-04-03 23:16:22.239 GcdTest[1406:c07] single = 21.546 msec 2014-04-03 23:16:22.239 GcdTest[1406:c07] 2014-04-03 23:16:25.388 GcdTest[1406:c07] wait = 0.009 msec 2014-04-03 23:16:25.388 GcdTest[1406:c07] topStart = 0.031 msec 2014-04-03 23:16:25.388 GcdTest[1406:c07] bottomStart = 0.057 msec 2014-04-03 23:16:25.389 GcdTest[1406:c07] 2014-04-03 23:16:25.389 GcdTest[1406:c07] topTime = 10.865 msec 2014-04-03 23:16:25.389 GcdTest[1406:c07] bottomeTime = 10.879 msec 2014-04-03 23:16:25.390 GcdTest[1406:c07] overallTime = 10.961 msec 

在模拟器上运行(.299r + .587g + .114b)

 2014-04-03 23:17:27.984 GcdTest[1422:c07] single = 55.738 msec 2014-04-03 23:17:27.985 GcdTest[1422:c07] 2014-04-03 23:17:29.306 GcdTest[1422:c07] wait = 0.008 msec 2014-04-03 23:17:29.307 GcdTest[1422:c07] topStart = 0.054 msec 2014-04-03 23:17:29.307 GcdTest[1422:c07] bottomStart = 0.060 msec 2014-04-03 23:17:29.307 GcdTest[1422:c07] 2014-04-03 23:17:29.308 GcdTest[1422:c07] topTime = 28.881 msec 2014-04-03 23:17:29.308 GcdTest[1422:c07] bottomeTime = 29.330 msec 2014-04-03 23:17:29.308 GcdTest[1422:c07] overallTime = 29.446 msec 

在iPad2上运行(r + g + b)/ 3

 2014-04-03 23:27:19.601 GcdTest[13032:907] single = 298.799 msec 2014-04-03 23:27:19.602 GcdTest[13032:907] 2014-04-03 23:27:20.536 GcdTest[13032:907] wait = 0.060 msec 2014-04-03 23:27:20.537 GcdTest[13032:907] topStart = 0.246 msec 2014-04-03 23:27:20.539 GcdTest[13032:907] bottomStart = 2.906 msec 2014-04-03 23:27:20.541 GcdTest[13032:907] 2014-04-03 23:27:20.542 GcdTest[13032:907] topTime = 149.596 msec 2014-04-03 23:27:20.544 GcdTest[13032:907] bottomeTime = 149.209 msec 2014-04-03 23:27:20.545 GcdTest[13032:907] overallTime = 152.164 msec 

在iPad2上运行(.299r + .587g + .114b)

 2014-04-03 23:30:29.618 GcdTest[13045:907] single = 282.767 msec 2014-04-03 23:30:29.620 GcdTest[13045:907] 2014-04-03 23:30:34.008 GcdTest[13045:907] wait = 0.046 msec 2014-04-03 23:30:34.010 GcdTest[13045:907] topStart = 0.270 msec 2014-04-03 23:30:34.011 GcdTest[13045:907] bottomStart = 3.043 msec 2014-04-03 23:30:34.013 GcdTest[13045:907] 2014-04-03 23:30:34.014 GcdTest[13045:907] topTime = 143.078 msec 2014-04-03 23:30:34.015 GcdTest[13045:907] bottomeTime = 143.249 msec 2014-04-03 23:30:34.017 GcdTest[13045:907] overallTime = 146.350 msec 

在iPad2上运行((。299r + .587g + .114b)^ 2.2)

 2014-04-03 23:41:28.959 GcdTest[13078:907] single = 1258.818 msec 2014-04-03 23:41:28.961 GcdTest[13078:907] 2014-04-03 23:41:30.768 GcdTest[13078:907] wait = 0.048 msec 2014-04-03 23:41:30.769 GcdTest[13078:907] topStart = 0.264 msec 2014-04-03 23:41:30.771 GcdTest[13078:907] bottomStart = 3.037 msec 2014-04-03 23:41:30.772 GcdTest[13078:907] 2014-04-03 23:41:30.773 GcdTest[13078:907] topTime = 635.952 msec 2014-04-03 23:41:30.775 GcdTest[13078:907] bottomeTime = 634.749 msec 2014-04-03 23:41:30.776 GcdTest[13078:907] overallTime = 637.829 msec 

在我的测试中,我发现如果我只专注于并发B&W转换,我实现了接近你所期望的“两倍速度”的东西(并行再现花费了53%,与串行再现一样长)。 当我还包括转换的辅助部分(不仅是转换,还有图像的检索,输出像素缓冲区的准备,以及新图像的创建等),那么最终的性能提升就不那么引人注目了,连续时间为连续时间的79%。

至于为什么你可能无法实现性能的绝对倍增,即使你只关注可以享受并发性的部分,Apple也会将这种行为归因于调度执行代码的开销。 在他们关于在并发编程指南中 同时使用“ 执行循环迭代”中使用dispatch_apply的讨论中他们考虑了并发任务的性能增益与每个调度块所需的开销之间的平衡:

您应该确保您的任务代码在每次迭代中都能完成合理的工作量。 与调度到队列的任何块或函数一样,调度该代码以执行也会产生开销。 如果循环的每次迭代只执行少量工作,则调度代码的开销可能会超过从将其分派到队列时可能获得的性能优势。 如果在测试期间发现这是真的,则可以使用跨步来增加每次循环迭代期间执行的工作量。 通过跨步,您可以将原始循环的多次迭代组合到一个块中,并按比例减少迭代次数。 例如,如果您最初执行100次迭代但决定使用4的步幅,则现在从每个块执行4次循环迭代,并且迭代计数为25.有关如何实现跨步的示例,请参阅“ 改进循环代码” 。 ”

顺便说一句,我认为可能值得考虑创建自己的并发队列并使用dispatch_apply 。 它专为此目的而设计, for可以享受并发性的循环进行优化。


这是我用于基准测试的代码:

 - (UIImage *)convertImage:(UIImage *)image algorithm:(NSString *)algorithm { CGImageRef imageRef = image.CGImage; NSAssert(imageRef, @"Unable to get CGImageRef"); CGDataProviderRef provider = CGImageGetDataProvider(imageRef); NSAssert(provider, @"Unable to get provider"); NSData *data = CFBridgingRelease(CGDataProviderCopyData(provider)); NSAssert(data, @"Unable to copy image data"); NSInteger bitsPerComponent = CGImageGetBitsPerComponent(imageRef); NSInteger bitsPerPixel = CGImageGetBitsPerPixel(imageRef); CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); NSInteger bytesPerRow = CGImageGetBytesPerRow(imageRef); NSInteger width = CGImageGetWidth(imageRef); NSInteger height = CGImageGetHeight(imageRef); CGColorSpaceRef colorspace = CGImageGetColorSpace(imageRef); void *outputBuffer = malloc(width * height * bitsPerPixel / 8); NSAssert(outputBuffer, @"Unable to allocate buffer"); uint8_t *buffer = (uint8_t *)[data bytes]; CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); if ([algorithm isEqualToString:kImageAlgorithmSimple]) { [self convertToBWSimpleFromBuffer:buffer toBuffer:outputBuffer width:width height:height]; } else if ([algorithm isEqualToString:kImageAlgorithmDispatchApply]) { [self convertToBWConcurrentFromBuffer:buffer toBuffer:outputBuffer width:width height:height count:2]; } else if ([algorithm isEqualToString:kImageAlgorithmDispatchApply4]) { [self convertToBWConcurrentFromBuffer:buffer toBuffer:outputBuffer width:width height:height count:4]; } else if ([algorithm isEqualToString:kImageAlgorithmDispatchApply8]) { [self convertToBWConcurrentFromBuffer:buffer toBuffer:outputBuffer width:width height:height count:8]; } NSLog(@"%@: %.2f", algorithm, CFAbsoluteTimeGetCurrent() - start); CGDataProviderRef outputProvider = CGDataProviderCreateWithData(NULL, outputBuffer, sizeof(outputBuffer), releaseData); CGImageRef outputImageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, outputProvider, NULL, NO, kCGRenderingIntentDefault); UIImage *outputImage = [UIImage imageWithCGImage:outputImageRef]; CGImageRelease(outputImageRef); CGDataProviderRelease(outputProvider); return outputImage; } /** Convert the image to B&W as a single (non-parallel) task. * * This assumes the pixel buffer is in RGBA, 8 bits per pixel format. * * @param inputButter The input pixel buffer. * @param outputBuffer The output pixel buffer. * @param width The image width in pixels. * @param height The image height in pixels. */ - (void)convertToBWSimpleFromBuffer:(uint8_t *)inputBuffer toBuffer:(uint8_t *)outputBuffer width:(NSInteger)width height:(NSInteger)height { for (NSInteger row = 0; row < height; row++) { for (NSInteger col = 0; col < width; col++) { NSUInteger offset = (col + row * width) * 4; uint8_t *rgba = inputBuffer + offset; uint8_t red = rgba[0]; uint8_t green = rgba[1]; uint8_t blue = rgba[2]; uint8_t alpha = rgba[3]; uint8_t gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue; outputBuffer[offset] = gray; outputBuffer[offset + 1] = gray; outputBuffer[offset + 2] = gray; outputBuffer[offset + 3] = alpha; } } } /** Convert the image to B&W, using GCD to split the conversion into several concurrent GCD tasks. * * This assumes the pixel buffer is in RGBA, 8 bits per pixel format. * * @param inputButter The input pixel buffer. * @param outputBuffer The output pixel buffer. * @param width The image width in pixels. * @param height The image height in pixels. * @param count How many GCD tasks should the conversion be split into. */ - (void)convertToBWConcurrentFromBuffer:(uint8_t *)inputBuffer toBuffer:(uint8_t *)outputBuffer width:(NSInteger)width height:(NSInteger)height count:(NSInteger)count { dispatch_queue_t queue = dispatch_queue_create("com.domain.app", DISPATCH_QUEUE_CONCURRENT); NSInteger stride = height / count; dispatch_apply(height / stride, queue, ^(size_t idx) { size_t j = idx * stride; size_t j_stop = MIN(j + stride, height); for (NSInteger row = j; row < j_stop; row++) { for (NSInteger col = 0; col < width; col++) { NSUInteger offset = (col + row * width) * 4; uint8_t *rgba = inputBuffer + offset; uint8_t red = rgba[0]; uint8_t green = rgba[1]; uint8_t blue = rgba[2]; uint8_t alpha = rgba[3]; uint8_t gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue; outputBuffer[offset] = gray; outputBuffer[offset + 1] = gray; outputBuffer[offset + 2] = gray; outputBuffer[offset + 3] = alpha; } } }); return YES; } void releaseData(void *info, const void *data, size_t size) { free((void *)data); } 

在iPhone 5上,使用简单的串行方法转换7360×4912图像需要2.24秒,当我使用带有两个循环的dispatch_apply时需要1.18秒。 当我尝试4或8个dispatch_apply循环时,我看到没有进一步的性能提升。