请教一个多线程CPU性能的问题

X_worm 2006-12-31 09:18:51
最近一个问题一直解决不了,找不到关键所在,希望各位高手给点建议:
我写了一个编码线程,用来独立地对视频图像进行实时编码,为了测试,我将视频图像固定为简单的静止图像序列,我用的是H.264 baseline profile编码标准,352*288大小,这样,假设在我的C1.7/512M机器上进行一路编码的CPU占用率5%左右,现在,我想同时进行两路编码,也就是互相独立地开两个同样的线程各自编码,于是问题出来了,CPU占用率直线上升,大部分情况下高达40%,当然偶尔也会降到10%左右。
这里需要说明的是我的解码线程是很独立的,不需要和其他线程同步,所以不存在同步开销。那为什么单路解码CPU占用不高,两路就高出那么多呢?如果单路5%两路15%也可以理解,但现在却是40%?

同样的问题,我还测试过我的解码线程,一般地,我开一个线程进行一路352*288大小25帧实时解码,占用CPU不超过2%,当我再增加一路变为2个线程解2路时CPU占用不超过5%,3个时一般不超过8%,但是当4个线程解4路时CPU又直线上升到了30%多?

到底是哪儿成了瓶颈呢?另外,我将线程中动态内存分配的地方改成开始时一次性创建私有堆分配内存,以避免共享内存访问的问题,但感觉性能影响不大,而且我改为不用堆而全部在堆栈内解码也不能解决上述问题。
我的感觉好像是一个进程的CPU占用率一旦达到某个值时再增加一个比较消耗CPU的线程,该进程的CPU占用率就不是量变了,而好像变成质变了,一下子提高了很多。
...全文
1801 点赞 收藏 21
写回复
21 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
argenCHN 2007-01-05
up
回复
yyzhao21 2007-01-05
mark
回复
Analyst 2007-01-04
主要问题集中在cache上,线程切换不会是主要问题。
一次cache失效的代价是几十到上百时钟周期,而线程切换代价是数千时钟周期,两者不在一个数量级上。并且两个解码线程是在两个核上跑,操作系统有线程亲缘性优化,这两个解码线程运行的核是不会被调换的,也就不会涉及线程上下文切换,至于其他非活动线程,被激活频率很低,可以忽略。
你的解码线程主要处理的是流数据,流数据的特点是要操作大量数据,这会对cache造成一定污染,处理不当会造成cache使用效率降低。但是CPU cache对程序员来说还是透明的,尽管SSE以后的指令集提供了几条cache操作指令,但是针对cache的优化手段目前还是比较有限的。存储系统的架构还有待进一步的改进。
另外还有一点忘了说,CPU占用率的测量并不是很准确的,有时候结果会有很大出入,比较精确的测量还是用CPU周期测量,建议用专门的profile工具或者自己插入profile代码来得到精确的测量。
回复
ren970122 2007-01-04
都是大师之言.
回复
shgmail 2007-01-04
顶一下
回复
wanglovec 2007-01-04
MARK
回复
X_worm 2007-01-03
谢谢各位,现在看来,我需要关注的重点一是缓存失败,二是线程切换开销。
回复
ProgrameMan 2007-01-02
3. 就是策率了,例如缓式计算、延迟创建等等,都输入程序性能优化的方面(当然满足你的要求)。
回复
ProgrameMan 2007-01-02
不过,有一点不明白,对于线程的上下文切换,理论上讲,不是只有我的线程才有上下文切换的,所有的线程当被分配到时间片时应该都有这个上下文切换,所以上下文切换的开销对所有的线程都是一样的吧?不管开销是大还是小?难道会不同的线程,上下文切换的开销会显著地差距很大?这一点我还不是很明白。我现在还是觉得上下文切换的开销对所有都是一样的。

************************************************************************************

应该是不一样的,我举一个例子你可能就明白了,例如,系统中运行了你的2个线程,其他的线程10个,这10个线程的代码中对内存的访问非常少,那么在这10个线程之间的上下文切换相对就是廉价的因为缓存失败或页面错误的机率就会低很多,但是当这些线程与你的线程进行上下文切换时就会存在
比较大的开销。


缓存的问题我现在觉得倒是值得我重点关注一下,那么,又有哪些需要注意的呢?是否,我将我的线程使用的所有内存都强制提交到物理内存,不允许被交换到磁盘上,这样对性能会有所改善呢?而对于CPU自身的高速缓存,我又能做些什么改善呢?

*******************************************************************************
个人观点:

1. 如你所说,提交到系统的物理内存是会改善页面错误的,
2. 在数据结构的设计上要使线程都要频繁访问的数据集中在连续的内存空间,例如:
你的线程中要使用两个全局类的两个实例

class Test
{
public:
int i;
char buffer[1024];
}

class Test1
{
public:
bool k;
int j;

}

假设你的线程重要频繁的访问 Test 中的 i 和 Test1中的 j ,而且往往是需要连续的访问
例如: TestObj.i++;
Test1Obj.j++;

那么我的建议是把 i 和 j 放到同一个 抽象的类中去使用,因为这样可以降低缓存失败的能性,当然这样做可能和 OO有些冲突,但我个人认为在性能关键的场合有时候是需要权衡的。
以上说的比较乱。对付看吧 呵呵







回复
DentistryDoctor 2007-01-01
多媒体方面的编解码,一般对(除CPU)系统资源比较敏感,但应该可以优化的。
回复
ProgrameMan 2007-01-01
WIN32靠线程的优先级(达到抢占式多任务的目的)及分配给线程的CPU时间来调度线程。WIN32本身的许多应用程序也利用了多线程的特性,如任务管理器等。

  本质而言,一个处理器同一时刻只能执行一个线程("微观串行")。WIN32多任务机制使得CPU好像在同时处理多个任务一样,实现了"宏观并行"。其多线程调度的机制为:

  (1)运行一个线程,直到被中断或线程必须等待到某个资源可用;

  (2)保存当前执行线程的描述表(上下文);

  (3)装入下一执行线程的描述表(上下文);

  (4)若存在等待被执行的线程,则重复上述过程。
回复
ProgrameMan 2007-01-01
1. 缓存失败?
内存访问的代价是相差很大的。在某种特定的RISC体系结构中,如果数据位于数据缓存中,那么数据访问需要一个CPU时钟周期;如果数据位于主存中(缓存失败),那么需要8个时钟周期;如果数据在磁盘中(页面错误),那么需要400,000个时钟周期,尽管确切的时钟周期数量可能不同,但不同的处理器体系结构在总的关系上是相同的;缓存成功、缓存失败和页面错误之间的速度(消耗CPU时钟)差别是不同数量级的差别。因此,楼主的问题有可能是多个线程处理数据时(而且每个线程对内存的访问非常频繁),产生了大量的缓存失败甚至是页面错误,这点可以通过性能监视器来分析。

2. 上下文切换?
上下文切换和缓存之间的交互对程序性能(消耗CPU时钟)的影响常常是最为有害的。结构和缓存类型对系统性能的这种特性产生显著的影响,缓存可以根据访问他们所使用的地址类型分成两类。虚拟编址缓存使用来自进程虚拟地址空间的地址访问。虚拟地址与进程无关。这就是说,系统中的每个进程都有可能使用虚拟地址100,但是虚拟地址100的物理定位将可能是几百个不同的物理地址。纯粹以来与虚拟地址的缓存在每次上下文切换时都需要刷新/失效。这将意味着每次上下文切换之后都需要重建缓存上下文。(最廉价的上下文重建需要至少100个时钟周期)


我想通过对上下文的简单阐述,大家还会说上下文的切换是很快的吗? 因为一个上下文切换至少要涉及缓存重建,它是非常昂贵的。

我针对楼主出现的现象,简单给出一个伪表达式,仅供参考:

1线程消耗的CPU时钟数 = 100000 CPU时钟数 + (上下文切换(按最小算) 100CPU时钟数 * 100次) = 110000
2线程消耗的CPU时钟数 = 100000 * 2 + (100CPU时钟数 * 200次) = 220000
3线程消耗的CPU时钟数 = 100000 * 3 + (100CPU时钟数 * 300次) = 330000

上面我的表达式是理想化的,大家可以想象一下,每次进行上下文切换时如果出现大量的缓存失败甚至是页面错误将会是什么结果。


以上所说如有错误请大家批评指正。 谢谢


回复
yjgx007 2007-01-01
在物理内存中缓存肯定是比在磁盘和内存中来回捣腾效率高的.
回复
X_worm 2007-01-01
谢谢楼上各位,尤其是ProgrameMan(我要学汇编)和yjgx007(听妈妈的话,向两星挺进! http://www.geekclaw.com),你们所说的对我很有帮助。
根据你们所言,我的问题很可能出在缓存和上下文切换上。
不过,有一点不明白,对于线程的上下文切换,理论上讲,不是只有我的线程才有上下文切换的,所有的线程当被分配到时间片时应该都有这个上下文切换,所以上下文切换的开销对所有的线程都是一样的吧?不管开销是大还是小?难道会不同的线程,上下文切换的开销会显著地差距很大?这一点我还不是很明白。我现在还是觉得上下文切换的开销对所有都是一样的。
yjgx007说:“如果打开这个注释段,每次切换的最大的消耗时间也不会超过400ms”,这个400ms的开销倒确实太大了,因为今天我不在公司,所以没法测试上面的程序。
缓存的问题我现在觉得倒是值得我重点关注一下,那么,又有哪些需要注意的呢?是否,我将我的线程使用的所有内存都强制提交到物理内存,不允许被交换到磁盘上,这样对性能会有所改善呢?而对于CPU自身的高速缓存,我又能做些什么改善呢?
还有,如果我对上下文切换的理解有误,又如何纠正并改善呢?
再次感谢各位!
回复
yjgx007 2007-01-01
我写了一个用于测试上下文切换的小程序,你可以打开下面代码中一个注释段, 为测试当磁盘交换时,可能需要重建线程上下文?可能这并不是一个好的方式,这主要想测试下你所说的:
“如果数据在磁盘中(页面错误),那么需要400,000个时钟周期,尽管确切的时钟周期数量可能不同,但不同的处理器体系结构在总的关系上是相同的;”
如果打开这个注释段,每次切换的最大的消耗时间也不会超过400ms
我的机器配置 CPU:celeron 1.3G RAM:641MB

我写的console程序,详细下载在:http://www.geekclaw.com/opensrc/SwitchContext.rar

#include "stdafx.h"
#include <afxwin.h>
#include <winnt.h>
#include <process.h>

HANDLE hCompetition = CreateEvent(NULL, TRUE, TRUE, "competition");
LARGE_INTEGER g_lastPerformanceCount;
LARGE_INTEGER g_frequency;
DWORD g_lastThreadId;

#define MAX_NUMBER_OF_THREADS 20
// If the above defined MAX_NUMBER_OF_THREADS is more bigger, you should properly
// reduce the number of the pages as below for preventing freezing with your computer.
#define MAX_EXCHANGE_PAGES 10000

void __cdecl threadProc(LPVOID pvoid)
{
SYSTEM_INFO stSysInfo;
GetSystemInfo(&stSysInfo);
DWORD dwProcessorPageSize = stSysInfo.dwPageSize;

int i = 100;
while (i--)
{
WaitForSingleObject(hCompetition, INFINITE);
// Here's switching to current context of the thread
if (g_lastThreadId != GetCurrentThreadId())
{
CString strFmt;
LARGE_INTEGER g_curPerformanceCount;
::QueryPerformanceCounter(&g_curPerformanceCount);
strFmt.Format("### The thread context from %u to % u ### Elapsed time to switch context: %I64u | %lfms\n",
g_lastThreadId, GetCurrentThreadId(),
(g_curPerformanceCount.QuadPart - g_lastPerformanceCount.QuadPart),
(double)(g_curPerformanceCount.QuadPart - g_lastPerformanceCount.QuadPart)/g_frequency.QuadPart*1000);
// OutputDebugString(strFmt);
printf(strFmt);
}
ResetEvent(hCompetition);

// There is exchanging data between physic memory and disk cache.
/* Comment out it for testing that there may need to rebuild the context of each thread.
* of course, it may not good solution for this, let me know if you have best solution.
*/
/*
int nSize = MAX_EXCHANGE_PAGES*dwProcessorPageSize;
BYTE* pBuff = (BYTE*)VirtualAlloc(NULL, nSize, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
if (pBuff != NULL)
{
ZeroMemory(pBuff, nSize);
VirtualFree(pBuff, 0, MEM_RELEASE);
}*/

// Reset last performance count
::QueryPerformanceCounter(&g_lastPerformanceCount);
g_lastThreadId = GetCurrentThreadId();
SetEvent(hCompetition);
}
}

int main(int argc, char* argv[])
{
HANDLE hThreadsArray[MAX_NUMBER_OF_THREADS];
// Before starting threads, we need to initialize
// the high-resolution frequency and last count assoicated with hardware.
QueryPerformanceFrequency(&g_frequency);
QueryPerformanceCounter(&g_lastPerformanceCount);
g_lastThreadId = GetCurrentThreadId();
for (int i = 0; i < MAX_NUMBER_OF_THREADS; ++i)
{
HANDLE hThread = (HANDLE)::_beginthread(threadProc, 0, NULL);
hThreadsArray[i] = hThread;
}

DWORD dwRet;
dwRet = ::WaitForMultipleObjects(MAX_NUMBER_OF_THREADS, hThreadsArray, TRUE, INFINITE);

MessageBox(NULL, "End the testing!", "SwitchContext", MB_OK);

return 0;
}
回复
X_worm 2006-12-31
谢谢楼上各位的解答!
不过,用多颗CPU的方案不是我决定的,将来是要由用户决定的。
为了快速地操作内存,我在线程一开始就通过CreateHeap()创建了这个线程专用的私有堆并一次性分配好所有内存资源,但仍然不能解决问题,包括全部使用堆栈也不行。
线程切换开销是不可避免的,如果我的线程没有很大的计算量,即使进程再创建100个线程,总的CPU占用应该也不会很高吧?
我的线程是编码线程,而且是H.264的编码,计算量是很大的,所以CPU占用确实很大,但应该不是1路5%两路就是40%吧。难道都是这样?
元旦后我用VC的profile功能统计分析一下,看看到底是哪个函数用去了那么多CPU?

这个帖子一周内结贴,谢谢各位
回复
yjgx007 2006-12-31
线程的上下文切换时间应该是很短暂的,楼主是不是长 时间CPU资源被大量占用?
回复
ProgrameMan 2006-12-31
怀疑是线程上下文切换导致的,线程越多开销越大效率就越低
回复
yjgx007 2006-12-31
如果是工作线程中需要不断地new和delete在堆中分配/释放内存,可以考虑用内存池,或者直接用VirtualAlloc保留一块较大内存,需要时递交,不需要时释放,这个比在堆中分配/释放要快一点。
回复
yjgx007 2006-12-31
建议用多个CPU处理,或者多核处理,
在单个CPU下,windows操作系统属于抢占式多任务模式,与实时操作系统不同,当你的一个编码主进程A开多个工作线程B1,B2,B3...Bn,这些工作线程需要消耗更多的CPU时间片,就是B1,B2,B3...Bn交替运行地占用CPU(并非同时运行的,除非使用多个CPU或多核,以达到真正的多线程同时处理)
回复
加载更多回复
相关推荐
发帖
进程/线程/DLL
创建于2007-09-28

1.5w+

社区成员

VC/MFC 进程/线程/DLL
申请成为版主
帖子事件
创建了帖子
2006-12-31 09:18
社区公告
暂无公告