设计一个扩展良好的多线程应用程序



下面的代码演示了我正在尝试做的事情,它与我的原始代码(此处未包含)存在相同的问题。我有谱图代码,我正试图通过使用多个线程来提高它的性能(我的计算机有4个内核)。声谱图代码基本上在许多重叠帧上计算FFT(这些帧对应于特定时间的声音样本)。

举个例子,假设我们有1000个重叠50%的帧。如果我们使用4个线程,那么每个线程应该处理250帧。重叠帧只是意味着,如果我们的帧长度为1024个样本帧具有范围0-1023、第二帧512-1535、第三帧1024-2047等(512个样本的重叠)。

创建和使用线程的代码

void __fastcall TForm1::Button1Click(TObject *Sender)
{
numThreads = 4;
fftLen = 1024;
numWindows = 10000;
int startTime = GetTickCount();
numOverlappingWindows = numWindows*2;
overlap = fftLen/2;
const unsigned numElem = fftLen*numWindows+overlap;
rx = new float[numElem];
for(int i=0; i<numElem; i++) {
rx[i] = rand();
}
useThreads = true;
vWThread.reserve(numOverlappingWindows);
if(useThreads){
for(int i=0;i<numThreads;i++){
TWorkerThread *pWorkerThread = new TWorkerThread(true); 
pWorkerThread->SetWorkerMethodCallback(&CalculateWindowFFTs);//this is called in TWorkerThread::Execute
vWThread.push_back(pWorkerThread);
}
pLock = new TCriticalSection();
for(int i=0;i<numThreads;i++){ //start the threads
vWThread.at(i)->Resume();
}
while(TWorkerThread::GetNumThreads()>0);
}else CalculateWindowFFTs();
int endTime = GetTickCount();
Label1->Caption = IntToStr(endTime-startTime);
}
void TForm1::CalculateWindowFFTs(){
unsigned startWnd = 0, endWnd = numOverlappingWindows, threadId;
if(useThreads){
threadId = TWorkerThread::GetCurrentThreadId();
unsigned wndPerThread = numOverlappingWindows/numThreads;
startWnd = (threadId-1)*wndPerThread;
endWnd   =  threadId*wndPerThread;
if(numThreads==threadId){
endWnd = numOverlappingWindows;
}
}
float *pReal, *pImg;
for(unsigned i=startWnd; i<endWnd; i++){
pReal = new float[fftLen];
pImg  = new float[fftLen];
memcpy(pReal, &rx[i*overlap], fftLen*sizeof(float));
memset(pImg, '0', fftLen);
FFT(pReal, pImg, fftLen);  //perform an in place FFT
pLock->Acquire();
vWndFFT.push_back(pReal);
vWndFFT.push_back(pImg);
pLock->Release();
}
}
void TForm1::FFT(float *rx, float *ix, int fftSize)
{
int i, j, k, m;
float rxt, ixt;
m = log(fftSize)/log(2);
int fftSizeHalf = fftSize/2;
j = k = fftSizeHalf;
for (i = 1; i < (fftSize-1); i++){
if (i < j) {
rxt = rx[j];
ixt = ix[j];
rx[j] = rx[i];
ix[j] = ix[i];
rx[i] = rxt;
ix[i] = ixt;
}
k = fftSizeHalf;
while (k <= j){
j = j - k;
k = k/2;
}
j = j + k;
}    //end for
int le, le2, l, ip;
float sr, si, ur, ui;
for (k = 1; k <= m; k++) {
le = pow(2, k);
le2 = le/2;
ur = 1;
ui = 0;
sr = cos(PI/le2);
si = -sin(PI/le2);
for (j = 1; j <= le2; j++) {
l = j - 1;
for (i = l; i < fftSize; i += le) {
ip = i + le2;
rxt = rx[ip] * ur - ix[ip] * ui;
ixt = rx[ip] * ui + ix[ip] * ur;
rx[ip] = rx[i] - rxt;
ix[ip] = ix[i] - ixt;
rx[i] = rx[i] + rxt;
ix[i] = ix[i] + ixt;
}    //end for
rxt = ur;
ur = rxt * sr - ui * si;
ui = rxt * si + ui * sr;
}
}
}

虽然很容易将此进程划分为多个线程,但与单线程版本相比,性能仅略有提高(<10%)。有趣的是,如果我把线程的数量增加到100个,我的速度确实会提高25%,这是令人惊讶的,因为我认为线程上下文切换开销是这种情况下的一个因素。

起初,我认为性能差的主要原因是对矢量对象的写入锁定,所以我用一个矢量数组(每个线程的矢量),从而减少了对锁的需求,但性能基本保持不变。

pVfft = new vector<float*>[numThreads];//create an array of vectors
//and then in CalculateWindowFFTs, do something like
vector<float*> &vThr = pVfft[threadId-1];
for(unsigned i=startWnd; i<endWnd; i++){
pReal = new float[fftLen];
pImg  = new float[fftLen];
memcpy(pReal, &rx[i*overlap], fftLen*sizeof(float));
memset(pImg, '0', fftLen);
FFT(pReal, pImg, fftLen);  //perform an in place FFT
vThr.push_back(pReal);      
}

我想我在这里遇到了缓存问题,尽管我不确定如何改变我的设计,以获得一个可以很好扩展的解决方案。

如果您认为TWorkerThread很重要,我也可以提供它的代码。

非常感谢您的帮助。

感谢

更新:

根据1201ProgramAlarm的建议,我去掉了while循环,使我的系统速度提高了15-20%。现在,我的主线程并没有主动等待线程完成,而是在所有工作线程完成后(即numThreads达到0时),让TWorkerThread通过TThread::Synchronize在主线程上执行代码。

虽然现在看起来更好了,但还远远没有达到最佳状态。

写入vWndFFT的锁会受到伤害,分配给pRealpImg的对new的重复(泄漏)调用也会受到伤害(这些调用应该在for循环之外)。

但真正的性能杀手可能是等待线程完成的循环:while(TWorkerThread::GetNumThreads()>0);。这将以一种非常不友好的方式消耗一个可用线程。

一个快速解决方案(不推荐)是添加sleep(1)(或2、5或10),这样循环就不连续了。

一个更好的解决方案是让主线程成为您的计算线程之一,并为该线程提供一种方法(一旦完成所有处理),只需等待另一个线程完成,而不消耗核心,使用Windows上可用的WaitForMultipleObjects之类的东西。

尝试线程化代码的一种简单方法是简单地运行线程化代码,但只使用一个线程。性能应该与非线程版本大致相同,并且结果应该匹配。

最新更新