11

我正在开发一个处理不同大小图像的程序。许多这些操作从输入读取像素数据并写入单独的输出(例如模糊)。这是在每个像素的基础上完成的。

这种图像映射对 CPU 的压力很大。我想使用多线程来加快速度。我该怎么做?我正在考虑每行像素创建一个线程。

我有几个要求:

  • 可执行文件的大小必须最小化。换句话说,我不能使用海量的库。什么是 C/C++ 最轻量级、可移植的线程库?
  • 可执行文件的大小必须最小化。我正在考虑有一个函数 forEachRow(fp* ) 为每一行运行一个线程,或者甚至是一个 forEachPixel(fp* ) ,其中 fp 在其自己的线程中对单个像素进行操作。哪个最好?
    • 我应该使用普通函数或仿函数或函数或一些 lambda 函数还是......其他什么?
    • 一些操作使用需要来自先前处理的像素的信息的优化。这使得 forEachRow 有利。即使考虑到这一点,使用 forEachPixel 会更好吗?
  • 我需要锁定我的只读和只写数组吗?
    • 输入只能从数组中读取,但许多操作需要从数组中的多个像素输入。
    • 每个像素仅写入一次输出。
  • 速度也很重要(当然),但优化可执行文件大小优先。

谢谢。

感兴趣的有关此主题的更多信息:C++ 并行化库:OpenMP 与线程构建块

4

16 回答 16

14

不要轻易开始穿线! 比赛条件可能是一个让人头疼的问题。特别是如果您没有很多线程经验! (您已被警告:这里有龙!毛茸茸的、不确定的、无法可靠复制的龙!)

你知道什么是死锁吗?活锁怎么样?

那就是说...


正如ckarmann 和其他人已经建议的那样: 使用工作队列模型。每个 CPU 内核一个线程。 将工作分成 N 个块。使块相当大,就像许多行一样。当每个线程空闲时,它会从队列中取出下一个工作块。

在最简单的IDEAL版本中,您有 N 个内核、N 个线程和问题的 N 个子部分,每个线程从一开始就知道它要做什么。

但由于启动/停止线程的开销,这在实践中通常不会发生。您真的希望线程已经生成并等待操作。(例如通过信号量。)

工作队列模型本身非常强大。它使您可以并行化诸如快速排序之类的事情,这通常不会在 N 个线程/内核之间优雅地并行化。


线程多于内核?你只是在浪费开销。每个线程都有开销。即使在#threads=#cores 下,您也永远无法获得完美的 Nx 加速因子。

每行一个线程将非常低效!每个像素一个线程?我什至不想考虑它。(在使用矢量化处理器单元时,这种逐像素方法更有意义,就像他们在旧 Cray 上使用的那样。但不是线程!)


图书馆?你的平台是什么?在 Unix/Linux/g++ 下,我建议使用 pthreads 和信号量。(Pthreads 也可以在带有 microsoft 兼容层的 windows 下使用。但是,uhgg。我不太相信它!Cygwin 在那里可能是一个更好的选择。)

在 Unix/Linux 下,man

* pthread_create, pthread_detach.
* pthread_mutexattr_init, pthread_mutexattr_settype, pthread_mutex_init,
* pthread_mutexattr_destroy, pthread_mutex_destroy, pthread_mutex_lock,
* pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutex_timedlock.
* sem_init, sem_destroy, sem_post, sem_wait, sem_trywait, sem_timedwait.

有些人喜欢 pthread 的条件变量。但我总是更喜欢 POSIX 1003.1b 信号量。他们处理您想要在另一个线程开始等待更好之前发出信号的情况。或者在另一个线程多次发出信号的地方。

哦,帮自己一个忙:将您的线程/互斥量/信号量 pthread 调用包装到几个 C++ 类中。这将简化很多事情!


我需要锁定我的只读和只写数组吗?

这取决于您的精确硬件和软件。通常只读数组可以在线程之间自由共享。但有些情况并非如此。

写法大同小异。通常,只要只有一个线程正在写入每个特定的内存点,就可以了。但有些情况并非如此!

写作比阅读更麻烦,因为您可能会陷入这些奇怪的围栏情况。内存通常写成字而不是字节。当一个线程写入单词的一部分,而另一个线程写入不同的部分时,取决于哪个线程在什么时候做什么的确切时间(例如不确定性),您可能会得到一些非常不可预测的结果!

我会谨慎行事:为每个线程提供其自己的读写区域副本。完成后,将数据复制回来。当然,都在互斥锁下。

除非您谈论的是千兆字节的数据,否则内存块非常快。那几微秒的性能时间根本不值得调试噩梦。

如果您要使用互斥锁在线程之间共享一个公共数据区域,那么冲突/等待互斥锁的低效率会堆积起来并破坏您的效率!


看,干净的数据边界是好的多线程代码的本质。当你的界限不明确时,那就是你遇到麻烦的时候。

同样,保持边界上的所有内容互斥也很重要!并保持互斥区域短!

尽量避免同时锁定多个互斥锁。如果您确实锁定了多个互斥体,请始终以相同的顺序锁定它们!

尽可能使用 ERROR-CHECKING 或 RECURSIVE 互斥锁。FAST 互斥锁只是自找麻烦,实际(测量)速度增益很少。

如果遇到死锁情况,请在 gdb 中运行它,按 ctrl-c,访问每个线程并回溯。通过这种方式,您可以很快找到问题。(活锁更难!)


最后一个建议:构建它单线程,然后开始优化。在单核系统上,您可能会发现自己从诸如 foo[i++]=bar ==> *(foo++)=bar 之类的东西中获得的速度比从线程中获得的速度更快。


附录: 我所说的保持互斥区域短于上方是什么意思?考虑两个线程:(给定一个 Mutex 类的全局共享互斥对象。)

/*ThreadA:*/ while(1){  mutex.lock();  printf("a\n");  usleep(100000); mutex.unlock(); }
/*ThreadB:*/ while(1){  mutex.lock();  printf("b\n");  usleep(100000); mutex.unlock(); }

会发生什么?

在我的 Linux 版本下,一个线程将连续运行,而另一个将饿死。当 mutex.unlock() 和 mutex.lock() 之间发生上下文交换时,它们很少会改变位置。


附录: 在您的情况下,这不太可能成为问题。但是对于其他问题,人们可能事先不知道完成一个特定的工作块需要多长时间。将问题分解为 100 个部分(而不是 4 个部分)并使用工作队列将其拆分为 4 个核心可以消除此类差异。

如果一个工作块的完成时间是另一个工作块的 5 倍,那么最终一切都会平息。虽然有太多的块,但获取新工作块的开销会造成明显的延迟。这是针对特定问题的平衡行为。

于 2008-11-29T04:04:26.113 回答
10

如果你的编译器支持OpenMP(我知道VC++ 8.0 和 9.0支持,gcc 也支持),它可以让这样的事情更容易做。

您不只是想创建很多线程 - 有一个收益递减点,当您开始获得越来越多的上下文切换时,添加新线程会减慢速度。在某些时候,使用太多线程实际上会使并行版本比仅使用线性算法更慢。最佳线程数是可用 CPU/内核数的函数,以及每个线程在 I/O 等事务上花费的时间百分比。查看Herb Sutter 的这篇文章,了解有关并行性能提升的一些讨论。

OpenMP 让您可以轻松地将创建的线程数调整为可用的 CPU 数。使用它(尤其是在数据处理情况下)通常只需#pragma omp在现有代码中放入一些 s,并让编译器处理创建线程和同步。

一般来说 - 只要数据没有变化,您就不必锁定只读数据。如果您可以确定每个像素槽只会被写入一次,并且您可以保证在开始读取结果之前已完成所有写入,那么您也不必锁定它。

对于 OpenMP,就函子/函数对象而言,不需要做任何特别的事情。以对您最有意义的方式编写它。这是来自英特尔的图像处理示例(将 rgb 转换为灰度):

#pragma omp parallel for
for (i=0; i < numPixels; i++)
{
   pGrayScaleBitmap[i] = (unsigned BYTE)
       (pRGBBitmap[i].red * 0.299 +
        pRGBBitmap[i].green * 0.587 +
        pRGBBitmap[i].blue * 0.114);
}

这会自动拆分为与 CPU 一样多的线程,并将数组的一部分分配给每个线程。

于 2008-11-28T19:50:31.947 回答
6

我会推荐boost::threadboost::gil(通用图像库)。因为涉及的模板很多,我不确定代码大小是否仍然可以为您所接受。但它是提升的一部分,所以它可能值得一看。

于 2008-11-28T19:31:08.193 回答
2

作为一个左场的想法......

你在什么系统上运行这个?您是否想过在您的 PC 中使用 GPU?

Nvidia 有用于这类事情的CUDA API

于 2008-11-28T19:39:50.173 回答
1

我认为您不希望每行有一个线程。可能有很多行,您将花费大量内存/CPU 资源来启动/销毁线程以及 CPU 从一个切换到另一个。此外,如果您拥有带有 C 内核的 P 处理器,那么您可能不会获得比 C*P 线程更多的收益。

我建议您使用定义数量的客户端线程,例如 N 个线程,并使用应用程序的主线程将行分配给每个线程,或者他们可以简单地从“作业队列”获取指令。当一个线程完成了一行时,它可以在这个队列中检查另一行来做。

至于库,您可以使用 boost::thread,它非常便携且不太重。

于 2008-11-28T19:33:24.867 回答
1

请问你是为哪个平台写的?我猜是因为可执行文件大小是一个你没有针对台式机的问题。在哪种情况下平台有多个内核或超线程?如果不是,那么向您的应用程序添加线程可能会产生相反的效果并减慢它的速度......

于 2008-11-28T19:37:35.920 回答
1

要优化简单的图像转换,使用 SIMD 矢量数学比尝试多线程程序要好得多。

于 2008-11-29T14:06:46.773 回答
1

您的编译器不支持 OpenMP。另一种选择是使用库方法,英特尔的线程构建块和 Microsoft 并发运行时都可用(VS 2010)。

还有一组称为并行模式库的接口,这两个库都支持这些接口,其中有一个模板化的 parallel_for 库调用。所以而不是:

#pragma omp parallel for 
for (i=0; i < numPixels; i++) 
{ ...} 

你会写:

parallel_for(0,numPixels,1,ToGrayScale());

其中 ToGrayScale 是函子或函数指针。(请注意,如果您的编译器支持 lambda 表达式,那么您可以将仿函数内联为 lambda 表达式)。

parallel_for(0,numPixels,1,[&](int i)
{  
   pGrayScaleBitmap[i] = (unsigned BYTE)  
       (pRGBBitmap[i].red * 0.299 +  
        pRGBBitmap[i].green * 0.587 +  
        pRGBBitmap[i].blue * 0.114);  
});

-瑞克

于 2010-01-15T15:55:26.847 回答
1

查看 MSDN 上的创建图像处理网络演练,其中解释了如何使用并行模式库来组成并发图像处理管道。

我还建议Boost.GIL,它可以生成高效的代码。对于简单的多线程示例,请查看Victor Bogado 的gil_threadedAn image processing network using Dataflow.Signals and Boost.GIL 也解释了一个有趣的数据流模型。

于 2012-09-28T15:02:10.327 回答
0

每个像素行一个线程是疯狂的,最好有大约 n-1 到 2n 个线程(对于 n 个 cpu),并使每个循环获取一个作业单元(可能是一行,或其他类型的分区)

在类 unix 上,使用 pthreads 它既简单又轻量级。

于 2008-11-28T19:33:48.127 回答
0

#ifdef也许编写您自己的小型库,该库使用's 为每个平台实现一些标准线程函数?它实际上并没有太多,这将比您可以使用的任何库更能减少可执行文件的大小。

更新:对于工作分配 - 将您的图像分成几块并给每个线程一块。所以当它完成了这件作品时,它就完成了。这样您就可以避免实施会进一步增加可执行文件大小的作业队列。

于 2008-11-28T19:50:23.117 回答
0

我认为无论您选择哪种线程模型(boost、pthread、本机线程等)。我认为你应该考虑一个线程池,而不是每行一个线程。线程池中的线程“启动”起来非常便宜,因为就操作系统而言,它们已经创建好了,只是给它一些事情要做。

基本上,您的池中可以说 4 个线程。然后以串行方式,对于每个像素,告诉线程池中的下一个线程来处理该像素。这样,您一次可以有效地处理不超过 4 个像素。您可以根据用户偏好或系统报告的 CPU 数量来确定池的大小。

恕我直言,这是迄今为止向 SIMD 任务添加线程的最简单方法。

于 2008-11-29T04:39:39.210 回答
0

很有可能,瓶颈不是 CPU,而是内存带宽,所以多线程不会有太大帮助。尽量减少内存访问并在有限的内存块上工作,以便可以缓存更多数据。不久前我遇到了类似的问题,我决定优化我的代码以使用 SSE 指令。每个单线程的速度提高了近 4 倍!

于 2010-01-15T10:54:30.270 回答
0

我认为 map/reduce 框架将是在这种情况下使用的理想之选。您可以使用 Hadoop 流来使用现有的 C++ 应用程序。

只需实施地图并减少工作。

正如您所说,您可以将行级操作用作映射任务,并将行级操作组合到 reduce 任务中的最终图像。

希望这是有用的。

于 2010-01-15T16:00:12.973 回答
0

您还可以使用IPPCassandra Vision C++ API 等库,这些库大多比您自己的代码优化得多。

于 2015-01-09T08:55:29.153 回答
-3

还有另一种使用汇编进行优化的选择。现在,一个令人兴奋的动态代码生成项目是softwire(可以追溯到一段时间 -是原始项目的站点)。它由 Nick Capens 开发,并发展成为现在可商用的swiftshader。但是原始软线的衍生产品仍然可以在 gna.org 上找到。

可以作为他的解决方案的介绍。

就个人而言,我不相信您可以通过使用多个线程来解决您的问题来获得显着的性能。

于 2008-11-28T21:50:56.243 回答