分类: C/C++
2008-04-23 21:54:00
C Q&A 专栏...
原著:Paul DiLascia
翻译:
原代码下载: (234KB)
原文出处:
分配大量小型类对象(如:10,000小型记录)最快和最佳方法是什么?
当然,MFC 序列流化对象可以完成所需的任务。但是,内存的分配和销毁相当耗时。有没有办法对此进行改进?
我无法告诉你最好的方法,因为那取决与应用程序的具体情况和其使用方式。性能和内存分配是如此巨大的一个主题,有关它们已经有很多很多书籍。没有哪一种方案适合所有的情形。最优化总是需要在速度和其它资源之间进行明智的权衡。例如,如果你愿意建立巨型索引,那么就会获得非常快的查询速度。或者要想显示速度快,那么就得以加载时间作为代价。因此,本文我只能就某些需要考虑的问题给你提供一个概述,以及提供一些工具和途径以帮助你自己找到答案。void CalculatePi()
{
ShowTime st("Calculating pi");
// do it
}
这段代码将产生一个象下面这样的 TRACE 信息:
Calculating pi: 342 msecShowTime 是如何工作的呢?它为智能指针以及在代码块起始处和末尾处你想做某些自动处理的地方使用常见的 C 构造函数/析构函数(ctor/dtor)模式。ShowTime 的构造函数将时钟时间(自从进程启动后的时钟嘀嗒数)保存在某个数据成员中;析构函数则用从最后的时钟数中减去这个时钟数并产生一条信息。由于构造函数/析构函数是在代码块的起始处/末尾处调用的,这样便测算出总共用了多少时间。代码如 Figure 1 所示。
// open log file
PerfLog mylog("MyResults.log");
现在 ShowTime 可以将信息写入MyResults.log文件以及TRACE流。但是,不要忘了在交付程序之前去掉这个性能监视。
有了 ShowTime 在手,我可以开始回答你的问题了。我写了一个小程序,PerfTest,这是一个典型的具备文档的 MFC
文档/视图应用,它使用三种不同的方法分配具有 20,000 条定长记录的链表。
方法一是典型的 MFC 方式。链表的实现使用 MFC 的 CObList,链表中的每个项目使用单独的表单元。每个表单元只是小小结构,此结构保存指向上下单元的指针和对象本身。所以
CObList 的每一项由12个字节的开销,但是,如果你需要几个表指向相同项目的话,就必须要有几个表单元。(例如,你想用不同的方法排序对象)。
方法二表示了第一个性能上的改进。这里记录本身存储下一条记录的指针,所以没有单独的表单元。这个方法在仅有一个链表的情况下才成为可能——也就是说,如果你不需要用几个链表来指向以不同方式排序的相同对象。在这样情况下,使用数组可能更有效。但即便是一个链表,如果要经常修改顺序,链表也比数组要快,因为修改指针比在内存中移动对象要快。
方法一和方法二都是每分配一个记录/对象单独调用一次 new 操作符。如果你分配20,000个对象,便调用20,000次 new
操作。方法三用单个数组一次性分配所有的 20,000 个对象:
m_array = new CMyRecord[20000];记录的链接则是通过设置每条记录中指向下一条记录的指针域实现的。分配的速度快,因为只有一次函数调用,但它需要一块连续的足以容纳 20,000 条记录的内存块。当然,编译器仍然要保证对象的初始化。当你用向量形式的 new 操作,编译器产生代码来调用每个对象的构造函数,因此有20,000次的构造函数调用。同样,在 delete [] 操作中会有 20,000 次的析构函数调用。如果构造函数/析构函数都为空,这些调用将被优化掉。但如果它们有实际的事可做,这个代码将需要有限次地执行。这时,你可能要进一步通过给该数组分配原始字节来加速性能(避免构造函数/析构函数调用),然后用手工编写代码来初始化这些对象——但现在这个对你已经不成问题。
class CMyRecord {
public:
void* operator new (size_t nbytes) {
return FancyAlloc(nbytes);
}
void operator delete (void* p) {
FancyFree(p);
}
};
你可以按照自己的意愿实现 FancyAlloc 和 FancyFree,只要它们按照正确的大小分配/释放内存块。如果你有一个在程序中全程使用的特殊对象,最常用的技巧之一是维护一个释放(free
pool)对象池。而不是去调用 free,你的 delete 操作符将释放的对象添加到一个叫释放池的链表中。然后分配器调用malloc之前在此释放池中查找对象。这样做可以使分配/释放操作极其快速,但你必须小心行事,使用内建的分配器而不能越雷池一步,大多数情况下它表现得相当不错。
我正在学习 Microsoft .NET
框架,不太理解控件和组件之间的差别。我知道这些术语可以互用,但什么时候从 Control 派生,什么时候从 Component 派生呢?
好问题!简单说来,控件就是具有用户界面的组件。要说的具体一点,就得回顾早期
Windows
的历史根源,当时控件指任何子窗口——按钮、列表框、编辑框或者某个对话框中的静态文本。从概念上讲,这些窗口——控件——类似用来操作收音机或小电器的旋钮和按钮。随着控件数量的增加(组合框、日期时间控件等等),控件逐渐成为子窗口的代名词,无论是用在对话框中还是用在其它种类的主窗口中。没过多久
BASIC
程序员开始编写他们自己专用的控件,自然而然地人们便想到共享这些控件。共享代码的方法之一是通过磁盘拷贝,但那样显然效率低下。必须要有一种机制使开发者建立的控件能够在其它程序员的应用中轻而易举地插入,这便是VBA控件,OLE控件,OCX和最后ActiveX
控件的动机。
// in CMyControl
[Category(S"Appearance")]
[Description(S"Specifies widget foreground color.")]
_property Color get_ForeColor() { ... }
_property void set_ForeColor(Color value) { ... }
现在窗体设计器在“外观”(Appearance)中列出你的 ForeColor
属性并使用帮助描述(Description)。有关设计时属性的更多内容,请参考.NET框架文档中的“组件的设计时属性”

Figure 5 类层次结构
Figure 5 显示了.NET框架中的类层次结构,它能说明上述讨论的问题。正如你所看到的,Control 从
Component
派生而来。这是用另外一种方式来说明控件即组件(反之则不然)。更具体地讲,控件是一个用用户界面的组件——能绘制东西并能与用户交互。Control
类还是所有托管窗口类的基类——窗体、按钮、栅格、面板、工具栏等等。Control 类是定义 WndProc 和 ClientSize
以及所有标准窗口事件如 GotFocus 和 Click 的地方。Web控件(System.Web.UI.Control)也是组件,不过从严格的意义上讲,它不是从 System.ComponentModel.Component
派生的。(对于 Web 控件,其名字空间为 System.Web.UI,Control 本身实现 IComponent。)
除了实现 IComponent 之外,System.ComponentModel.Component
还提供了所有组件需要的列集支持,但它是通过从 MarshalByRefObject 派生来实现的。如果想生成一个值列集组件,可以从 MarshalByValueComponent
派生(它实现了 IComponent,IDisposable 和 IServiceProvider)。System.Data.DataColumn,DataSet
和 DataTable 都是是值列集组件的例子。这些对象跨机器/进程边界传递其实际数据。
如果你正在编写其他人也能用窗体设计器拖拽到其窗体的可重用的小组件,那么你必须从 Component
派生。如果你的小组件还具备用户界面——能创建窗口,绘画或与用户交互——那么就应该从 Control 派生。明白了吗?
向 Paul 提问和评论请发到 cppqa@microsoft.com.
作者简介