我有一个将拜耳图像通道转换为RGB的算法。在我的实现中,我有一个嵌套的for
循环,它遍历拜耳通道,从拜耳索引计算rgb索引,然后从拜耳通道设置该像素的值。
这里要注意的主要事实是每个像素可以独立于其他像素计算(不依赖于先前的计算),因此该算法是并行化的自然候选者。然而,计算依赖于一些预设数组,所有线程将在同一时间访问但不会更改。
然而,当我尝试将主for
与MS的cuncurrency::parallel_for
并行化时,我的性能没有提升。实际上,对于在4核CPU上运行的大小为3264X2540的输入,非并行化版本运行~34ms,并行版本运行~69ms(平均10次运行)。我确认操作确实是并行化的(为该任务创建了3个新线程)。
将英特尔的编译器与tbb::parallel_for
一起使用可获得接近完全的结果。
为了进行比较,我开始使用C#
中实现的算法,其中我也使用了parallel_for
循环,在那里我遇到了接近X4的性能提升(我选择C++
因为这个特定的任务{ {1}}即使使用单核也更快。
有什么想法阻止我的代码很好地并行化?
我的代码:
C++
答案 0 :(得分:22)
首先,您的算法受限于内存带宽。那就是内存加载/存储将超过你所做的任何索引计算。
SSE
/ AVX
之类的矢量操作也无济于事 - 您没有进行任何密集计算。
每次迭代增加工作量也没用 - PPL
和TBB
都足够聪明,不能每次迭代创建线程,他们会使用一些好的分区,这将另外尝试保留局部性。例如,这里引用TBB::parallel_for
:
当工作线程可用时,
parallel_for
执行迭代是非确定性的顺序。不要依赖任何特定的执行顺序来保证正确性。但是,为了提高效率,确实期望parallel_for倾向于在连续的值运行中运行。
真正重要的是减少内存操作。对输入或输出缓冲区进行任何多余的遍历都会导致性能下降,因此您应该尝试删除memset
或同时执行此操作。
您正在完全遍历输入和输出数据。即使你跳过输出中的东西 - 这也不重要,因为内存操作是在现代硬件上由64字节块发生的。因此,计算输入和输出的size
,衡量算法的time
,除以size
/ time
并将结果与系统的最大特征进行比较(例如,衡量benchmark)。
我已经对Microsoft PPL
,OpenMP
和Native for
进行了测试,结果是(我使用了你身高的8倍):
Native_For 0.21 s
OpenMP_For 0.15 s
Intel_TBB_For 0.15 s
MS_PPL_For 0.15 s
如果删除memset
,则:
Native_For 0.15 s
OpenMP_For 0.09 s
Intel_TBB_For 0.09 s
MS_PPL_For 0.09 s
正如您所看到的,memset
(经过高度优化)可以响应大量的执行时间,这可以显示您的算法是如何受内存限制的。
#include <boost/exception/detail/type_info.hpp>
#include <boost/mpl/for_each.hpp>
#include <boost/mpl/vector.hpp>
#include <boost/progress.hpp>
#include <tbb/tbb.h>
#include <iostream>
#include <ostream>
#include <vector>
#include <string>
#include <omp.h>
#include <ppl.h>
using namespace boost;
using namespace std;
const auto Width = 3264;
const auto Height = 2540*8;
struct MS_PPL_For
{
template<typename F,typename Index>
void operator()(Index first,Index last,F f) const
{
concurrency::parallel_for(first,last,f);
}
};
struct Intel_TBB_For
{
template<typename F,typename Index>
void operator()(Index first,Index last,F f) const
{
tbb::parallel_for(first,last,f);
}
};
struct Native_For
{
template<typename F,typename Index>
void operator()(Index first,Index last,F f) const
{
for(; first!=last; ++first) f(first);
}
};
struct OpenMP_For
{
template<typename F,typename Index>
void operator()(Index first,Index last,F f) const
{
#pragma omp parallel for
for(auto i=first; i<last; ++i) f(i);
}
};
template<typename T>
struct ConvertBayerToRgbImageAsIs
{
const T* BayerChannel;
T* RgbChannel;
template<typename For>
void operator()(For for_)
{
cout << type_name<For>() << "\t";
progress_timer t;
int offsets[] = {2,1,1,0};
//memset(RgbChannel, 0, Width * Height * 3 * sizeof(T));
for_(0, Height, [&] (int row)
{
for (auto col = 0, bayerIndex = row * Width; col < Width; col++, bayerIndex++)
{
auto offset = (row % 2)*2 + (col % 2); //0...3
auto rgbIndex = bayerIndex * 3 + offsets[offset];
RgbChannel[rgbIndex] = BayerChannel[bayerIndex];
}
});
}
};
int main()
{
vector<float> bayer(Width*Height);
vector<float> rgb(Width*Height*3);
ConvertBayerToRgbImageAsIs<float> work = {&bayer[0],&rgb[0]};
for(auto i=0;i!=4;++i)
{
mpl::for_each<mpl::vector<Native_For, OpenMP_For,Intel_TBB_For,MS_PPL_For>>(work);
cout << string(16,'_') << endl;
}
}
答案 1 :(得分:5)
我猜想每次循环完成的工作量太小。如果您将图像分成四个部分并且并行运行计算,您会注意到一个很大的增益。 尝试以减少迭代次数和每次迭代更多工作的方式设计循环。这背后的原因是同步太多了。
一个重要因素可能是如何对数据进行拆分(分区)以进行处理。 如果处理的行分开,如下面的错误情况,则更多行将导致缓存未命中。对于每个附加线程,此效果将变得更加重要,因为行之间的距离将更大。如果您确定并行化功能执行合理的分区,则手动分工不会产生任何结果
bad good
****** t1 ****** t1
****** t2 ****** t1
****** t1 ****** t1
****** t2 ****** t1
****** t1 ****** t2
****** t2 ****** t2
****** t1 ****** t2
****** t2 ****** t2
另外,请确保以与对齐方式相同的方式访问您的数据;每次调用offset[]
和BayerChannel[]
都可能是缓存未命中。您的算法非常耗费内存。几乎所有操作都是访问内存段或写入内存段。防止缓存未命中和最小化内存访问至关重要。
下面显示的优化可能由编译器完成,可能无法提供更好的结果。值得知道的是,他们可以完成。
// is the memset really necessary?
//memset(RgbChannel, 0, Width * Height * 3 * sizeof(T));
parallel_for(0, Height, [&] (int row)
{
int rowMod = (row & 1) << 1;
for (auto col = 0, bayerIndex = row * Width, tripleBayerIndex=row*Width*3; col < Width; col+=2, bayerIndex+=2, tripleBayerIndex+=6)
{
auto rgbIndex = tripleBayerIndex + offsets[rowMod];
RgbChannel[rgbIndex] = BayerChannel[bayerIndex];
//unrolled the loop to save col & 1 operation
rgbIndex = tripleBayerIndex + 3 + offsets[rowMod+1];
RgbChannel[rgbIndex] = BayerChannel[bayerIndex+1];
}
});
答案 2 :(得分:3)
我的建议来了:
展开内循环以计算一个完整像素(简化代码)
template<typename T> void static ConvertBayerToRgbImageAsIsNew(T* BayerChannel, T* RgbChannel, int Width, int Height)
{
// convert BGGR->RGB
// have as many threads as the hardware concurrency is
parallel_for(0, Height, static_cast<int>(Height/(thread::hardware_concurrency())), [&] (int stride)
{
for (auto row = stride; row<2*stride; row++)
{
for (auto col = row*Width, rgbCol =row*Width; col < row*Width+Width; rgbCol +=3, col+=4)
{
RgbChannel[rgbCol+0] = BayerChannel[col+3];
RgbChannel[rgbCol+1] = BayerChannel[col+1];
// RgbChannel[rgbCol+1] += BayerChannel[col+2]; // this line might be left out if g is used unadjusted
RgbChannel[rgbCol+2] = BayerChannel[col+0];
}
}
});
}
此代码比原始版本快60%,但仍然只是笔记本电脑上非并行版本的一半。这似乎是由于算法的记忆有限性,正如其他人已经指出的那样。
编辑:但我对此并不满意。从parallel_for
到std::async
时,我可以大大提高并行性能:
int hc = thread::hardware_concurrency();
future<void>* res = new future<void>[hc];
for (int i = 0; i<hc; ++i)
{
res[i] = async(Converter<char>(bayerChannel, rgbChannel, rows, cols, rows/hc*i, rows/hc*(i+1)));
}
for (int i = 0; i<hc; ++i)
{
res[i].wait();
}
delete [] res;
转换器是一个简单的类:
template <class T> class Converter
{
public:
Converter(T* BayerChannel, T* RgbChannel, int Width, int Height, int startRow, int endRow) :
BayerChannel(BayerChannel), RgbChannel(RgbChannel), Width(Width), Height(Height), startRow(startRow), endRow(endRow)
{
}
void operator()()
{
// convert BGGR->RGB
for(int row = startRow; row < endRow; row++)
{
for (auto col = row*Width, rgbCol =row*Width; col < row*Width+Width; rgbCol +=3, col+=4)
{
RgbChannel[rgbCol+0] = BayerChannel[col+3];
RgbChannel[rgbCol+1] = BayerChannel[col+1];
// RgbChannel[rgbCol+1] += BayerChannel[col+2]; // this line might be left out if g is used unadjusted
RgbChannel[rgbCol+2] = BayerChannel[col+0];
}
};
}
private:
T* BayerChannel;
T* RgbChannel;
int Width;
int Height;
int startRow;
int endRow;
};
现在比非并行版本快3.5倍。从我到目前为止在Profiler中看到的情况来看,我认为parallel_for的工作窃取方法会导致大量的等待和同步开销。
答案 3 :(得分:2)
我没有使用过tbb :: parallel_for cuncurrency :: parallel_for,但是如果你的数字是正确的,它们似乎带来了太多的开销。但是,我强烈建议您在测试时运行10次迭代,并确保在计时之前进行尽可能多的预热迭代。
我使用三种不同的方法测试了您的代码,平均超过1000次尝试。
Serial: 14.6 += 1.0 ms
std::async: 13.6 += 1.6 ms
workers: 11.8 += 1.2 ms
首先是连续计算。第二个是使用对std :: async的四次调用完成的。最后一步是通过向四个已经启动(但正在休眠)的后台线程发送四个作业来完成的。
收益并不大,但至少它们是收益。我在2012年的MacBook Pro上进行了测试,双超线程内核= 4个逻辑内核。
供参考,这是我的std :: async parallel for:
template<typename Int=int, class Fun>
void std_par_for(Int beg, Int end, const Fun& fun)
{
auto N = std::thread::hardware_concurrency();
std::vector<std::future<void>> futures;
for (Int ti=0; ti<N; ++ti) {
Int b = ti * (end - beg) / N;
Int e = (ti+1) * (end - beg) / N;
if (ti == N-1) { e = end; }
futures.emplace_back( std::async([&,b,e]() {
for (Int ix=b; ix<e; ++ix) {
fun( ix );
}
}));
}
for (auto&& f : futures) {
f.wait();
}
}
答案 4 :(得分:2)
要检查或做的事
memset
,正如其他人所解释的那样。其他时间
我在一个简单的C ++ 03 Win23实现中合并了Evgeny Panasyuk和ogni42的建议:
#include "stdafx.h"
#include <omp.h>
#include <vector>
#include <iostream>
#include <stdio.h>
using namespace std;
const int Width = 3264;
const int Height = 2540*8;
class Timer {
private:
string name;
LARGE_INTEGER start;
LARGE_INTEGER stop;
LARGE_INTEGER frequency;
public:
Timer(const char *name) : name(name) {
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
}
~Timer() {
QueryPerformanceCounter(&stop);
LARGE_INTEGER time;
time.QuadPart = stop.QuadPart - start.QuadPart;
double elapsed = ((double)time.QuadPart /(double)frequency.QuadPart);
printf("%-20s : %5.2f\n", name.c_str(), elapsed);
}
};
static const int offsets[] = {2,1,1,0};
template <typename T>
void Inner_Orig(const T* BayerChannel, T* RgbChannel, int row)
{
for (int col = 0, bayerIndex = row * Width;
col < Width; col++, bayerIndex++)
{
int offset = (row % 2)*2 + (col % 2); //0...3
int rgbIndex = bayerIndex * 3 + offsets[offset];
RgbChannel[rgbIndex] = BayerChannel[bayerIndex];
}
}
// adapted from ogni42's answer
template <typename T>
void Inner_Unrolled(const T* BayerChannel, T* RgbChannel, int row)
{
for (int col = row*Width, rgbCol =row*Width;
col < row*Width+Width; rgbCol +=3, col+=4)
{
RgbChannel[rgbCol+0] = BayerChannel[col+3];
RgbChannel[rgbCol+1] = BayerChannel[col+1];
// RgbChannel[rgbCol+1] += BayerChannel[col+2]; // this line might be left out if g is used unadjusted
RgbChannel[rgbCol+2] = BayerChannel[col+0];
}
}
int _tmain(int argc, _TCHAR* argv[])
{
vector<float> bayer(Width*Height);
vector<float> rgb(Width*Height*3);
for(int i = 0; i < 4; ++i)
{
{
Timer t("serial_orig");
for(int row = 0; row < Height; ++row) {
Inner_Orig<float>(&bayer[0], &rgb[0], row);
}
}
{
Timer t("omp_dynamic_orig");
#pragma omp parallel for
for(int row = 0; row < Height; ++row) {
Inner_Orig<float>(&bayer[0], &rgb[0], row);
}
}
{
Timer t("omp_static_orig");
#pragma omp parallel for schedule(static)
for(int row = 0; row < Height; ++row) {
Inner_Orig<float>(&bayer[0], &rgb[0], row);
}
}
{
Timer t("serial_unrolled");
for(int row = 0; row < Height; ++row) {
Inner_Unrolled<float>(&bayer[0], &rgb[0], row);
}
}
{
Timer t("omp_dynamic_unrolled");
#pragma omp parallel for
for(int row = 0; row < Height; ++row) {
Inner_Unrolled<float>(&bayer[0], &rgb[0], row);
}
}
{
Timer t("omp_static_unrolled");
#pragma omp parallel for schedule(static)
for(int row = 0; row < Height; ++row) {
Inner_Unrolled<float>(&bayer[0], &rgb[0], row);
}
}
printf("-----------------------------\n");
}
return 0;
}
以下是我在三通道8路超线程Core i7-950盒子上看到的时间:
serial_orig : 0.13
omp_dynamic_orig : 0.10
omp_static_orig : 0.10
serial_unrolled : 0.06
omp_dynamic_unrolled : 0.04
omp_static_unrolled : 0.04
“静态”版本告诉编译器在循环入口处平均分配线程之间的工作。这避免了尝试进行工作窃取或其他动态负载平衡的开销。对于此代码段,即使跨线程的工作负载非常一致,它似乎没有什么区别。
答案 5 :(得分:0)
性能降低可能正在发生,因为您正在尝试在“行”数量的核心上分配循环,这些核心不可用,因此它再次成为具有并行开销的顺序执行。
答案 6 :(得分:0)
对并行for循环不是很熟悉,但在我看来,争用是在内存访问中。您的线程看起来是对相同页面的重叠访问。
你可以将数组访问分解为4k块,与页面边界对齐吗?
答案 7 :(得分:0)
在没有优化串行代码的for循环之前,没有必要讨论并行性能。这是我的尝试(一些好的编译器可能能够获得类似的优化版本,但我宁愿不依赖它)
parallel_for(0, Height, [=] (int row) noexcept
{
for (auto col=0, bayerindex=row*Width,
rgb0=3*bayerindex+offset[(row%2)*2],
rgb1=3*bayerindex+offset[(row%2)*2+1];
col < Width; col+=2, bayerindex+=2, rgb0+=6, rgb1+=6 )
{
RgbChannel[rgb0] = BayerChannel[bayerindex ];
RgbChannel[rgb1] = BayerChannel[bayerindex+1];
}
});