摘要: 前几篇文章中,我们实现了配置文件的读写,界面的动态展现,这次我们来实现界面内选项的动态展示。由于属于C#界面跟脚本的交互功能,所以,这次的功能完全可以撇开整个功能设计而单独讨论。本次项目的脚本采用Lua。Lua在Dotnet上的不是很多,目前主要有两个开源的类库LuaNet和LuaInterface,但这两个类库都不是很完善,那我们就扬长避短,明知有Bug的地方就尽量回避。值得庆幸的是,在界面交互...阅读全文
posted @ 2010-05-16 22:05 feedback 阅读(967) 评论(5) 编辑

  前一篇,我们定义并实现了配置文件的读写功能,这次我们来实现配置文件的界面展示。之前我们设计的时候,就要求界面能够动态展现,比如在界面定义文件中新增一个CheckBox定义,那么要求界面能够反映出来。

本次界面还原技术,我们采用SharpDev的方式,利用xml文件对界面进行描述,然后利用反射将界面展示出来。(PS:如果你用过Delphi,你同样会发现Delphi的frm文件也是采用这样的思路),这里有个问题需要暂时搁置一下,对于界面中用到的图像资源的处理,目前暂时不用管。

好了,我们先来看一下设计思路

总界面管理类定义

代码
    /// <summary>
    
/// 界面管理类
    
/// </summary>
    public interface IConfigUiManager
    {
        
/// <summary>
        
/// 默认界面文件路径
        
/// </summary>
        string DefaultFloder
        {
            
get;
            
set;
        }
        
/// <summary>
        
/// 界面描述文件的后缀
        
/// </summary>
        string DefaultExt
        {
            
get;
            
set;
        }
        
/// <summary>
        
/// 扫描默认目录下的界面文件并加载
        
/// </summary>
        void Load();
        
/// <summary>
        
/// 定位某个界面
        
/// </summary>
        
/// <param name="pName"></param>
        
/// <returns></returns>
        IConfigSectionUi FindUi(string pName);
        
/// <summary>
        
/// 展示总配置界面
        
/// </summary>
        void ShowUi();
    }

 

 当个配置程序节的界面定义

 

代码
    /// <summary>
    
/// 单个配置程序节的界面
    
/// </summary>
    public interface IConfigSectionUi
    {
        
/// <summary>
        
/// 是否集成到总界面中
        
/// </summary>
        bool LazyLoad
        {
            
get;
            
set;
        }
        
/// <summary>
        
/// 界面名称
        
/// </summary>
        string Name
        {
            
get;
            
set;
        }
        
/// <summary>
        
///从配置文件中加载选项
        
/// </summary>
        void LoadFromConfig();
        
/// <summary>
        
///  将设置保存到配置文件中
        
/// </summary>
        void SaveToConfig();
        
/// <summary>
        
/// 从xml文件中提取的界面
        
/// </summary>
        System.Windows.Forms.Control Ui
        {
            
get;
            
set;
        }
        
/// <summary>
        
/// 展示当前的配置界面
        
/// </summary>
        void ShowUi();
        
void LoadFromFile(string pFileName);
        
string Caption
        {
            
get;
            
set;
        }
        Control FindControl(
string pCtlName);

    }

 

在这里,我们需要声明一下,对于界面定义文件,我们支持两种方式,一种是Form形式定义,这样的方式允许程序在运行期弹出独立界面跟用户交互;其二,是UserControl方式,这样的方式运行两种模式运行,第一集成到配置总界面中集中管理,第二,单独的弹出界面跟用户交互;基于这样的考虑,所以,我在接口IConfigSectionUi中定义了标识LazyLoad。

基本类的定义就是上面两个,但是辅助类还没有定义,这里,我不妨直接借用SharpDev的代码来实现

这里用到了其中核心的一个类XmlLoader(具体实现可以参考其源代码)

  好了,这样一来,界面的还原功能就实现了。但还是没有达到我们之前制定的目标。好吧,我们来实现IConfigSectionUi.LoadFromFile方法

 

代码
        XmlLoader xmlLoader = null;

        
void IConfigSectionUi.LoadFromFile(string pFileName)
        {
            
using (FileStream pStream = new FileStream(pFileName, FileMode.Open))
            {
                AppConfig.Ui.FakeForm FakeForm 
= new AppConfig.Ui.FakeForm();
                
object FakeObj = null;

                
if (pStream == null)
                {
                    
throw new System.ArgumentNullException("stream");
                }
                FakeForm.SuspendLayout();
                xmlLoader 
= new XmlLoader();
                SetupXmlLoader();
                
if (pStream != null)
                {
                    FakeObj 
= xmlLoader.LoadObjectFromStream(FakeForm, pStream);
                }
                
if (FakeForm.Controls.Count > 0)
                {
                    
this._Ui = FakeForm;
                    
this._Name = FakeForm.Name;
                    
this._LazyLoad = true;
                    
this._Caption = FakeForm.Caption;
                    FakeForm.ResumeLayout(
false);
                }
                
else
                {
                    
this._Ui = (Control)FakeObj;
                    
this._Name = _Ui.Name;
                    
this._Caption = ((Ui.FakeUserControl)_Ui).Caption;
                }
                pStream.Close();
            }
        }

 

  嗯,我们按照两种方式,将界面Form和UserControl区分开了,那么在展现的做一下手脚就可以了。

看一下总界面的展现方法实现

 

代码
        void IConfigUiManager.ShowUi()
        {
            Ui.FormConfigMain FormConfigMain 
= new AppConfig.Ui.FormConfigMain();
            TabControl pTab 
= FormConfigMain.pcCtl;
            
foreach (KeyValuePair<string, IConfigSectionUi> pUi in _List)
            {
                
if (pUi.Value.LazyLoad)
                {
                    
// pUi.Value.ShowUi();
                }
                
else//加入总界面
                {
                    TabPage pPage 
= new TabPage();
                    pPage.Text 
= pUi.Value.Caption;
                    pPage.Controls.Add(pUi.Value.Ui);
                    pUi.Value.Ui.Dock 
= DockStyle.Fill;
                    pTab.TabPages.Add(pPage);
                }
            }
            FormConfigMain.ShowDialog();
        }

 

如果发现此界面为单独界面即Form类型,则不加载到总界面中;

嗯,那么单个界面展示如何展示呢?看一下吧

 

代码
        void IConfigSectionUi.ShowUi()
        {
            
if (this._Ui == nullreturn;
            
if (_LazyLoad)
                
this._Ui.Show();
            
else
            {
                Ui.FormConfigDynamic frm 
= new AppConfig.Ui.FormConfigDynamic();
                frm.pnlCtl.Controls.Add(
this._Ui);
                frm.Show();
            }
        }

 

如果此界面为单独界面,直接Show出来,如果是个UserControl,则用内置的ormConfigDynamic界面包装一下然后Show出来。

好了,到此为止,基本实现了我们之前定义的需求。

展示一下几个测试界面

好了,到此,界面的动态展示基本完成。下次,我们将解决界面内选项的动态展示,也就是涉及到脚本技术跟C#的互动。

(未完待续...)

posted @ 2010-05-16 21:33 feedback 阅读(250) 评论(0) 编辑

  上次,我们分析了“动态”配置文件的基本需求,也基本定下了设计思路。我们今天就来实现第一要素:读写配置文件

  按照我们上次的分析要求,我设计了以下几个类

ConfigManager,【配置文件管理类】

Config,【单个配置文件】

ConfigSection,【配置文件中的单个程序节】

三个类依次包含,为了方便程序调用,为此,设计了三个接口,分别由三个类来实现。这样一来,外部调用的程序,只需要知道接口,而不需要引用这个类;

部分定义代码

IConfigManager定义

IConfigManager
 1     /// <summary>
 2     /// 配置文件管理接口
 3     /// </summary>
 4     public interface IConfigManager
 5     {
 6         /// <summary>
 7         /// 增加配置文件
 8         /// </summary>
 9         /// <param name="pCfg">配置文件</param>
10         void AddConfig(IConfig pCfg);
11         /// <summary>
12         /// 删除配置文件
13         /// </summary>
14         /// <param name="pCfg">配置文件</param>
15         void DeleteConfig(IConfig pCfg);
16         /// <summary>
17         /// 定位配置文件
18         /// </summary>
19         /// <param name="pName"></param>
20         /// <returns></returns>
21         IConfig FindConfig(string pName);
22         /// <summary>
23         /// 配置文件是否存在
24         /// </summary>
25         /// <param name="pName"></param>
26         /// <returns></returns>
27         bool HasExist(string pName);
28         /// <summary>
29         /// 默认配置文件后缀
30         /// </summary>
31         string DefaultEx
32         {
33             get;
34             set;
35         }
36         /// <summary>
37         /// 默认配置文件存放位置
38         /// </summary>
39         string DefaultFolder
40         {
41             get;
42             set;
43         }
44         /// <summary>
45         /// 加载目录下的所有配置文件
46         /// </summary>
47         void Load();
48         /// <summary>
49         /// 当前的配置文件
50         /// </summary>
51         IConfig CurrentConfig
52         {
53             get;
54             set;
55         }
56     }

 

IConfig定义

 

代码
 1     /// <summary>
 2     /// 单个配置文件
 3     /// </summary>
 4     public interface IConfig
 5     {
 6         /// <summary>
 7         /// 配置文件名称
 8         /// </summary>
 9         string Name
10         {
11             get;
12             set;
13         }
14         /// <summary>
15         /// 增加程序节
16         /// </summary>
17         /// <param name="pCfgSec"></param>
18         void AddSection(IConfigSection pCfgSec);
19         /// <summary>
20         /// 删除程序节
21         /// </summary>
22         /// <param name="pCfgSec"></param>
23         void DeleteSection(IConfigSection pCfgSec);
24         /// <summary>
25         /// 定位程序节
26         /// </summary>
27         /// <param name="pName"></param>
28         /// <returns></returns>
29         IConfigSection FindSection(string pName);
30         /// <summary>
31         /// 查询程序节是否存在
32         /// </summary>
33         /// <param name="pName"></param>
34         /// <returns></returns>
35         bool HasExist(string pName);
36         /// <summary>
37         /// 直接读取某个程序节下的设置
38         /// </summary>
39         /// <param name="pSectionName"></param>
40         /// <param name="pKeyName"></param>
41         /// <param name="pDefaultValue"></param>
42         /// <returns></returns>
43         object ReadKeyValue(string pSectionName, string pKeyName, object pDefaultValue);
44         /// <summary>
45         /// 写入某个程序节下,如果此程序节不存在,将创建
46         /// </summary>
47         /// <param name="pSectionName"></param>
48         /// <param name="pKeyName"></param>
49         /// <param name="pValue"></param>
50         void WriteKeyValue(string pSectionName, string pKeyName, object pValue);
51         /// <summary>
52         /// 加载文件,读取全部配置
53         /// </summary>
54         /// <param name="pFileName"></param>
55         void LoadfromFile(string pFileName);
56         /// <summary>
57         /// 设置保存到文件
58         /// </summary>
59         void SaveToFile();
60         /// <summary>
61         /// 自动保存
62         /// </summary>
63         bool AutoSave
64         {
65             get;
66             set;
67         }
68         /// <summary>
69         /// 是否开启文件监测
70         /// </summary>
71         bool Watched
72         {
73             get;
74             set;
75         }
76         /// <summary>
77         /// 此配置文件的文件路径
78         /// </summary>
79         string FileName
80         {
81             get;
82             set;
83         }
84     }

 

IConfigSection定义

 

代码
    /// <summary>
    
/// 配置分类
    
/// </summary>
    public interface IConfigSection
    {
        
string SectionName
        {
            
get;
            
set;
        }
        
void Add(string pKeyName, object pKeyValue);
        
void Delete(string pKeyName);
        
object ReadKeyValue(string pKeyName, object pDefaultValue);
        
void WriteKeyValue(string pKeyName, object pValue);
        
void SaveToFile();
    }

 

 

由于本次采用ini文件作为配置文件的载体,所以,在读写上就省略了很多方法。甚至在IConfig接口上提供了读写方法,但为了XML文件之类的读写,还是提供了方法定义。

(未完待续...)

posted @ 2010-05-16 20:55 feedback 阅读(810) 评论(1) 编辑

  几乎所有的程序都会用到配置文件,我们对配置文件的需求只有2个,保存用户的选择,下次界面展示的时候还原。这是我们使用配置的唯一的也是最重要的目的,如果这点达不到,就应该放弃所采用的方式。目前,比较常用的配置文件方式有注册表、ini文件、xml,C#下常用的还有app.config或者Web.config文件,这个文件本质还是xml,只是fx封装了读写。

  我们需要讨论的是如何方便的读写配置文件,以及展示配置界面。所谓的“动态”是包含2个方面,其一指配置的内容是动态的,易于添加和删除的,这需要跟界面展示配合,其二,所配置的选项也是动态的,如下拉框的内容并不是设计期可以确定,而是在程序运行中来进行确定。当然,如果期望纯代码区维护,也是可以而且可行,只是太繁琐了。那有没有一种解决方案,可以达到这样的效果,当程序更新后,需要在配置文件中新增一个配置项时候,在界面展示的时候可以自动体现出来呢?今天我们就来探讨一下。

  第一、读写配置文件。我们可以设计一个类来管理配置文件中所有的节点,不妨定义为ConfigManager,它至少需要提供以下方法,增删改定位Section,增删改Key、KeyValue

Section表示一个环节,Key表示一个设置项,KeyValue表示一个设置项的数值。任何一种配置文件几乎都能满足这些要求,我们采用了ini文件来进行,这样方便项目发布;

  第二、界面展示。由于要求为“动态”,所以,展示的界面就不能在程序内部定义。我们可以定义一个ConfigUiManager类,它需要保存和还原单个或者多个界面,我们的项目中涉及到的配置界面应该会有单个或者多个,如Office一样,可以集中设置,也可以单独在某项处理的时候弹出对应功能的设置界面。满足这样的需求,我们可以采用序列化的方式来进行,也可以采用SharpDev的方式,用xml来定义窗体控件和布局,然后在运行期来展示。

  第三、界面事件。如下拉框的选项,那么来源只有在运行期才能确定。这就需要用到脚本技术了,我们对此脚本的要求是运行期绑定事件和执行,目前我采用的是Lua脚本。当然,通过cs代码,然后运行期编译称dll然后加载执行,也是可行的方式,只是保密性差了点。

  第三、界面和配置文件的关联。我们需要把配置文件的设置反映到界面上,同时也需要把界面的设置保存到配置文件中。我们不妨定义为ConfigLinker。

 (未完待续...)

posted @ 2010-05-15 18:35 feedback 阅读(156) 评论(0) 编辑

  好久好久没关注微软技术的发展了。随着VS2010的发布以及fx4.0的推出,很多新技术新概念也出来了。期待已久的AE10还没有出现,我估摸着也快了,这次ESRI肯定会跟着MS发布组合fx4.0的版本,甚至于ESRI可以提供3.5以及4.0两个版本,让程序员自由选择,而没有必要绑死在一个版本下面进行开发。

  好几年前,ESRI就宣言今后的重点是WebGIS方向,现在社会的主流也是这个,那这次AE10到底会带给我们什么呢?众所周知,MS的SL技术模仿Flex到了极致,这次SL3.0更是炉火纯青,ESRI又将如何面对SL开发,有没有像Flex一样提供更方便的接口或者类库呢,我们拭目以待。

  今后的GIS发展方向,数据编辑放在CS端,剩下的所有操作全部搬上BS吧,有了Flex,SL,你还担心什么?

  VS2010估计会跟ESRI的发展有点冲突,VS2008就结合的挺好。目前市场上大部分ESRI产品还在沿用AE9.2,我们记得,从9.0到9.1,然后到9.2,现在是9.3,我们发现一个很奇怪的现象,9.0用的挺广,9.1较少,9.2直接接替了9.0,然后现在的9.3用的其实也不多,主要在Server这一块比较多,大部分产品继续使用着9.2.期望这次AE10的推出,能够将CS开发和BS开发版本做一次统一。

  有理由相信,随着ESRI ArcGIS10的发布,江湖将会出现一阵血雨腥风。新事物的更替,总会伴随着阵痛,没有关系,我们相信长江后浪推前浪。ArcGIS10,我们期待你的到来……

posted @ 2010-05-09 23:47 feedback 阅读(87) 评论(0) 编辑

  用了这么些年电脑,鼠标不知道用坏了多少个。市场上的鼠标挺便宜的,15块钱1个,随便用。这些烂鼠标往往用了几个月之后就挂掉了。我一直怀念我很久之前用的老鼠标,那会罗技还没这么多的型号,我记得,当时好像是一个套装,白色的鼠标+白色的键盘,非常经典,非常好用,一直很喜欢罗技键盘的大回车键,每次写完程序,都会恶狠狠地猛敲一下回车,整个办公室都能听到这声音,贼爽~~~时隔多年,这个键盘已经停产,而且想买也买不到。唯独剩下当时的鼠标作为回忆。

    回忆归回忆,如果还能废物利用,也不失为一件幸事。其实,鼠标内部构造很简单,我手上还没有镊子,当然了,最好有个镊子,那样会很方便。拆开之后,注意里面的滚轮安装顺序,如果没有网络,用相机拍下来,免得后面不会安装。最后要注意的是那个小盒子里面的弹簧,也要先观察怎么安装的,然后再拆下来。一般鼠标用的时间久了,都是那个弹簧接头处发生了氧化,拿下来,用砂纸磨一下,或者用小螺丝刀轻轻的磨口一下就行。然后原样装回去。如果手上有酒精,给鼠标内部做个清理,太脏了不太好。

  我用这个的办法,让2个留在回忆中的鼠标复活了~

PS:而且呢,如果以后鼠标左键再发生问题,我可以把两个鼠标合成一个使用。这样不是挺好的嘛~~~

posted @ 2010-05-09 19:53 feedback 阅读(154) 评论(1) 编辑

VS2010RC1终于出来了,抢先试用~

  由于是在Win7上直接安装,所以,界面自然是花哨无比,但我试用了几个项目,发现一个小问题。VS2010里面的TreeView类型的东西,比如属性、项目管理树,他们的节点并不是原先丑陋的"+"和"-",而是变成了上翻和下翻的小按钮,挺可爱,于是,就想看看WinForm里面是不是也跟着一起改变了,至少提供界面模式吧~非常的失望,MS没这么干,它只顾自己享受,没有考虑程序员的开发量,如果想要类似的效果,还得自己手工绘制或者借助三方控件,可恶~~~~~~~~~

     VS2010自带了fx4,这点其实不好。因为win7自带的是3.5,也就是说,如果发布2010的程序给用户,还得在用户机器上安装fx4.0,可悲啊~~有没有现成的虚拟机可以自带?做到跟人家飞信一样,多好,多方便……PS:当然了,由于2010可以选择编译的fx版本,如无必要,可以不采用4.0的fx,但往往羡慕4.0的新功能,就会导致我上述的痛苦。

   2010的WPF和SL,很好,很强大,暂缓体验。由于目前在使用Flex4,所以,这块不太关心。

  令人惊喜的是2010还带了一个新东东,F#。每次MS发布新东东的时候,我都激动,但往往结果都比较令人失望,在经历无数次SP之后,才稍微令人满意。有创意是不错,但要做好才行。那这次的F#究竟如何呢,我们来边玩边学。

   对于F#,有个专门的网站http://www.fsharp.net/,(这是一个官方的镜像)同时,在官方也有地址可以学习范例http://code.msdn.microsoft.com/fsharpsamples

这点做的挺好,毁人不倦。根据官方的介绍,这个语言的定位是介于脚本语言跟编译语言之间的,但我看来,这就是一个脚本语言。这应该是ms看到了脚本语言的广阔市场,所以才发明了这样的一个语言。MS啊,你每次都是领悟的太晚。我们来看看这个语言能带给我们多少惊喜吧~

1.官方的例子说是可以直接操作Excel,Word等,这点其他脚本语言也是可以的嘛,大伙都是通过COM来操作,不看不看

2。第一个例子是个控制台程序,打开选项中的显示行数,设置断点,可以调试,很好,很强大。

3,第二个例子是个WinForm的例子,没有界面文件,只有一些描述文件,这点完完全全的脚本,用脚本来描述界面,然后挂接事件,哎~不知道说什么才好了。

4。第三个是脚本控制。还没整明白怎么挂接宿主。应该是有Host来call才行的。

5.3D功能。可惜的是我机器没有安装DX9SDK,跑步起来,但是看看官方的运行效果图,很赞~

其他貌似没有了。

但我想,这个语言的出现,能做的事情并不仅仅局限于上述这些功能,借助于.NET Framework这个平台,这个脚本语言可以做的更好更强大,而目前,脚本是所有开发中必不可少的一个环节,期望MS不要让大家失望~PS:本人目前使用LUA作为脚本语言,小巧、轻便、速度还挺快,我很满意,F#如果没有预想中那么满意,我不会换脚本的~

最后,贴几个F#的图吧,解解馋~

  

posted @ 2010-05-09 18:06 feedback 阅读(1231) 评论(3) 编辑

  笔记本用的时间长了,眼睛有点受不了,毕竟屏幕太小,亮度太低,最近发现视力有点下降ing,再加上家里旧的台式机性能不行,尽管前几年多多少少的升级过,唯一能看得上就只有一个19的显示器,其他的都要淘汰了。基于上述原因,打算重新配置一台主机。

  对电脑市场做了一个初步的分析,目前正处在一个更新换代的风口上,配机有点犹豫。最近的内存硬盘价格一路疯涨,我都无语了。明年3月份i3将会出现,四核将会进入大众阶段(当然明年的主流应该是6核了),现在的显卡也处在一个过渡阶段,明年将会全面支持dx11,但现在只有A卡支持,而且是阉割版的,水分极大,现在购买,时机不对。

  针对上面情况,我列了一个配置单

    AMD 955+技嘉770T+2G内存+500G硬盘+HD4860;

显卡,我本想换HD4890VX的,价格应该是跟4890差不多,但散热性能好了很多,但市场上没货(而且预定也不行),无奈之下,只能选择4860;

CPU,本来用620,超频到3.2使用。后来,电话咨询之后,原来可以预定,于是改换了955,这款主频高,如果以后超频,也比较好超一点;

主板770T,对付一般超频够用了,不打算用790,觉得有点浪费;

内存,目前价格偏高,先2G凑合着,等以后再加;

硬盘500G够用了,不需要追求太大的空间,稳定是第一位;

电源400W差不多了,就算显卡超频也基本够用;

----------------------------------------------------------------------------------------------------------------------

这样一来,四核3A平台基本搭建完毕,也基本没什么瓶颈,虽然955配4860,有点对不起这个U,但超频之后应该还是可以的;

----------------------------------------------------------------------------------------------------------------------

  现在Win7出来有一段时间了,据说口碑还不错。我这人跟时代严重脱节,一直都在用XP,连Vista都没有用过。这次的升级,我打算改用Win7,虽然win7分32位版本和64位版本,但win7的服务器版2008R2却只有64位。64位是以后的主流,据微软称XP的维护到2014年结束,这意味着32位平台也差不多走到头了,最多还有5-8年的光景。作为一个开发人员,需要紧跟时代脚步啊。

  以上面的机器配置,我估计用个3年应该没有问题,可能唯一的缺点就是显卡上面,但本人不做游戏开发,所以也就无所谓了。

posted @ 2009-12-28 08:18 feedback 阅读(91) 评论(0) 编辑

级别: 中级

杨 小华 (normalnotebook@126.com), 软件工程师

2008 年 11 月 17 日

大家也许还记得 2005 年 3 月 C++ 大师 Herb Sutter 在 Dr.Dobb’s Journal 上发表了一篇名为《免费的午餐已经结束》的文章。文章指出:现在的程序员对效率、伸缩性、吞吐量等一系列性能指标相当忽视,很多性能问题都仰仗越来越快的 CPU 来解决。但 CPU 的速度在不久的将来,即将偏离摩尔定律的轨迹,并达到一定的极限。所以,越来越多的应用程序将不得不直面性能问题,而解决这些问题的办法就是采用并发编程技术。

样例程序

程序功能:求从1一直到 APPLE_MAX_VALUE (100000000) 相加累计的和,并赋值给 apple 的 ab ;求 orange 数据结构中的 a[i]+b[i ] 的和,循环 ORANGE_MAX_VALUE (1000000) 次。

说明:

  1. 由于样例程序是从实际应用中抽象出来的模型,所以本文不会进行 test.a=test.b= test.b+sum 、中间变量(查找表)等类似的优化。
  2. 以下所有程序片断均为部分代码,完整代码请参看本文最下面的附件。

清单 1. 样例程序
#define ORANGE_MAX_VALUE      1000000
#define APPLE_MAX_VALUE       100000000
#define MSECOND               1000000

struct apple
{
     unsigned long long a;
	unsigned long long b;
};

struct orange
{
	int a[ORANGE_MAX_VALUE];
	int b[ORANGE_MAX_VALUE];
	
};

int main (int argc, const char * argv[]) {
    // insert code here...
     struct apple test;
	struct orange test1;
	
	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
	{
		test.a += sum;
		test.b += sum;
	}
	
     sum=0;
	for(index=0;index<ORANGE_MAX_VALUE;index++)
	{
		sum += test1.a[index]+test1.b[index];
	}

     return 0;
}





回页首


K-Best 测量方法

在检测程序运行时间这个复杂问题上,将采用 Randal E.Bryant 和 David R. O’Hallaron 提出的 K 次最优测量方法。假设重复的执行一个程序,并纪录 K 次最快的时间,如果发现测量的误差 ε 很小,那么用测量的最快值表示过程的真正执行时间, 称这种方法为“ K 次最优(K-Best)方法”,要求设置三个参数:

K: 要求在某个接近最快值范围内的测量值数量。

ε 测量值必须多大程度的接近,即测量值按照升序标号 V1, V2, V3, … , Vi, … ,同时必须满足(1+ ε)Vi >= Vk

M: 在结束测试之前,测量值的最大数量。

按照升序的方式维护一个 K 个最快时间的数组,对于每一个新的测量值,如果比当前 K 处的值更快,则用最新的值替换数组中的元素 K ,然后再进行升序排序,持续不断的进行该过程,并满足误差标准,此时就称测量值已经收敛。如果 M 次后,不能满足误差标准,则称为不能收敛。

在接下来的所有试验中,采用 K=10,ε=2%,M=200 来获取程序运行时间,同时也对 K 次最优测量方法进行了改进,不是采用最小值来表示程序执行的时间,而是采用 K 次测量值的平均值来表示程序的真正运行时间。由于采用的误差 ε 比较大,在所有试验程序的时间收集过程中,均能收敛,但也能说明问题。

为了可移植性,采用 gettimeofday() 来获取系统时钟(system clock)时间,可以精确到微秒。





回页首


测试环境

硬件:联想 Dual-core 双核机器,主频 2.4G,内存 2G

软件:Suse Linunx Enterprise 10,内核版本:linux-2.6.16





回页首


软件优化的三个层次

医生治病首先要望闻问切,然后才确定病因,最后再对症下药,如果胡乱医治一通,不死也残废。说起来大家都懂的道理,但在软件优化过程中,往往都喜欢犯这样的错误。不分青红皂白,一上来这里改改,那里改改,其结果往往不如人意。

一般将软件优化可分为三个层次:系统层面,应用层面及微架构层面。首先从宏观进行考虑,进行望闻问切,即系统层面的优化,把所有与程序相关的信息收集上来,确定病因。确定病因后,开始从微观上进行优化,即进行应用层面和微架构方面的优化。

  1. 系统层面的优化:内存不够,CPU 速度过慢,系统中进程过多等
  2. 应用层面的优化:算法优化、并行设计等
  3. 微架构层面的优化:分支预测、数据结构优化、指令优化等

软件优化可以在应用开发的任一阶段进行,当然越早越好,这样以后的麻烦就会少很多。

在实际应用程序中,采用最多的是应用层面的优化,也会采用微架构层面的优化。将某些优化和维护成本进行对比,往往选择的都是后者。如分支预测优化和指令优化,在大型应用程序中,往往采用的比较少,因为维护成本过高。

本文将从应用层面和微架构层面,对样例程序进行优化。对于应用层面的优化,将采用多线程和 CPU 亲和力技术;在微架构层面,采用 Cache 优化。





回页首


并行设计

利用并行程序设计模型来设计应用程序,就必须把自己的思维从线性模型中拉出来,重新审视整个处理流程,从头到尾梳理一遍,将能够并行执行的部分识别出来。

可以将应用程序看成是众多相互依赖的任务的集合。将应用程序划分成多个独立的任务,并确定这些任务之间的相互依赖关系,这个过程被称为分解(Decomosition)。分解问题的方式主要有三种:任务分解、数据分解和数据流分解。关于这部分的详细资料,请参看参考资料一。

仔细分析样例程序,运用任务分解的方法 ,不难发现计算 apple 的值和计算 orange 的值,属于完全不相关的两个操作,因此可以并行。

改造后的两线程程序:


清单 2. 两线程程序
void* add(void* x)
{		
	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
	{
		((struct apple *)x)->a += sum;
		((struct apple *)x)->b += sum;	
	}
		
	return NULL;
}
	
int main (int argc, const char * argv[]) {
		// insert code here...
	struct apple test;
	struct orange test1={{0},{0}};
	pthread_t ThreadA;
		
	pthread_create(&ThreadA,NULL,add,&test);
		
	for(index=0;index<ORANGE_MAX_VALUE;index++)
	{
		sum += test1.a[index]+test1.b[index];
	}		
	
     pthread_join(ThreadA,NULL);

	return 0;
}

更甚一步,通过数据分解的方法,还可以发现,计算 apple 的值可以分解为两个线程,一个用于计算 apple a 的值,另外一个线程用于计算 apple b 的值(说明:本方案抽象于实际的应用程序)。但两个线程存在同时访问 apple 的可能性,所以需要加锁访问该数据结构。

改造后的三线程程序如下:


清单 3. 三线程程序
struct apple
{
     unsigned long long a;
	unsigned long long b;
	pthread_rwlock_t rwLock;
};

void* addx(void* x)
{
	pthread_rwlock_wrlock(&((struct apple *)x)->rwLock);
	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
	{
		((struct apple *)x)->a += sum;
	}
	pthread_rwlock_unlock(&((struct apple *)x)->rwLock);
	
	return NULL;
}

void* addy(void* y)
{
	pthread_rwlock_wrlock(&((struct apple *)y)->rwLock);
	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
	{
		((struct apple *)y)->b += sum;
	}
	pthread_rwlock_unlock(&((struct apple *)y)->rwLock);
	
	return NULL;
}



int main (int argc, const char * argv[]) {
    // insert code here...
     struct apple test;
	struct orange test1={{0},{0}};
	pthread_t ThreadA,ThreadB;
	
	pthread_create(&ThreadA,NULL,addx,&test);
	pthread_create(&ThreadB,NULL,addy,&test);

	for(index=0;index<ORANGE_MAX_VALUE;index++)
	{
		sum+=test1.a[index]+test1.b[index];
	}
	
     pthread_join(ThreadA,NULL);
     pthread_join(ThreadB,NULL);
	
     return 0;
}

这样改造后,真的能达到我们想要的效果吗?通过 K-Best 测量方法,其结果让我们大失所望,如下图:


图 1. 单线程与多线程耗时对比图
单线程与多线程耗时对比图

为什么多线程会比单线程更耗时呢?其原因就在于,线程启停以及线程上下文切换都会引起额外的开销,所以消耗的时间比单线程多。

为什么加锁后的三线程比两线程还慢呢?其原因也很简单,那把读写锁就是罪魁祸首。通过 Thread Viewer 也可以印证刚才的结果,实际情况并不是并行执行,反而成了串行执行,如图2:


图 2. 通过 Viewer 观察三线程运行情况
通过 Viewer 观察三线程运行情况

其中最下面那个线程是主线程,一个是 addx 线程,另外一个是 addy 线程,从图中不难看出,其他两个线程为串行执行。

通过数据分解来划分多线程,还存在另外一种方式,一个线程计算从1到 APPLE_MAX_VALUE/2 的值,另外一个线程计算从 APPLE_MAX_VALUE/2+1APPLE_MAX_VALUE 的值,但本文会弃用这种模型,有兴趣的读者可以试一试。

在采用多线程方法设计程序时,如果产生的额外开销大于线程的工作任务,就没有并行的必要。线程并不是越多越好,软件线程的数量尽量能与硬件线程的数量相匹配。最好根据实际的需要,通过不断的调优,来确定线程数量的最佳值。





回页首


加锁与不加锁

针对加锁的三线程方案,由于两个线程访问的是 apple 的不同元素,根本没有加锁的必要,所以修改 apple 的数据结构(删除读写锁代码),通过不加锁来提高性能。

测试结果如下:


图 3. 加锁与不加锁耗时对比图
加锁与不加锁耗时对比图

其结果再一次大跌眼镜,可能有些人就会越来越糊涂了,怎么不加锁的效率反而更低呢?将在针对 Cache 的优化一节中细细分析其具体原因。

在实际测试过程中,不加锁的三线程方案非常不稳定,有时所花费的时间相差4倍多。

要提高并行程序的性能,在设计时就需要在较少同步和较多同步之间寻求折中。同步太少会导致错误的结果,同步太多又会导致效率过低。尽量使用私有锁,降低锁的粒度。无锁设计既有优点也有缺点,无锁方案能充分提高效率,但使得设计更加复杂,维护操作困难,不得不借助其他机制来保证程序的正确性。





回页首


针对 Cache 的优化

在串行程序设计过程中,为了节约带宽或者存储空间,比较直接的方法,就是对数据结构做一些针对性的设计,将数据压缩 (pack) 的更紧凑,减少数据的移动,以此来提高程序的性能。但在多核多线程程序中,这种方法往往有时会适得其反。

数据不仅在执行核和存储器之间移动,还会在执行核之间传输。根据数据相关性,其中有两种读写模式会涉及到数据的移动:写后读和写后写 ,因为这两种模式会引发数据的竞争,表面上是并行执行,但实际只能串行执行,进而影响到性能。

处理器交换的最小单元是 cache 行,或称 cache 块。在多核体系中,对于不共享 cache 的架构来说,两个独立的 cache 在需要读取同一 cache 行时,会共享该 cache 行,如果在其中一个 cache 中,该 cache 行被写入,而在另一个 cache 中该 cache 行被读取,那么即使读写的地址不相交,也需要在这两个 cache 之间移动数据,这就被称为 cache 伪共享,导致执行核必须在存储总线上来回传递这个 cache 行,这种现象被称为“乒乓效应”。

同样地,当两个线程写入同一个 cache 的不同部分时,也会互相竞争该 cache 行,也就是写后写的问题。上文曾提到,不加锁的方案反而比加锁的方案更慢,就是互相竞争 cache 的原因。

在 X86 机器上,某些处理器的一个 cache 行是64字节,具体可以参看 Intel 的参考手册。

既然不加锁三线程方案的瓶颈在于 cache,那么让 apple 的两个成员 ab 位于不同的 cache 行中,效率会有所提高吗?

修改后的代码片断如下:


清单 4. 针对Cache的优化
struct apple
{
	unsigned long long a;
	char c[128];  /*32,64,128*/
	unsigned long long b;
};

测量结果如下图所示:


图 4. 增加 Cache 时间耗时对比图
增加 Cache 时间耗时对比图

小小的一行代码,尽然带来了如此高的收益,不难看出,我们是用空间来换时间。当然读者也可以采用更简便的方法: __attribute__((__aligned__(L1_CACHE_BYTES))) 来确定 cache 的大小。

如果对加锁三线程方案中的 apple 数据结构也增加一行类似功能的代码,效率也是否会提升呢?性能不会有所提升,其原因是加锁的三线程方案效率低下的原因不是 Cache 失效造成的,而是那把锁。

在多核和多线程程序设计过程中,要全盘考虑多个线程的访存需求,不要单独考虑一个线程的需求。在选择并行任务分解方法时,要综合考虑访存带宽和竞争问题,将不同处理器和不同线程使用的数据放在不同的 Cache 行中,将只读数据和可写数据分离开。





回页首


CPU 亲和力

CPU 亲和力可分为两大类:软亲和力和硬亲和力。

Linux 内核进程调度器天生就具有被称为 CPU 软亲和力(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。但不代表不会进行小范围的迁移。

CPU 硬亲和力是指进程固定在某个处理器上运行,而不是在不同的处理器之间进行频繁的迁移。这样不仅改善了程序的性能,还提高了程序的可靠性。

从以上不难看出,在某种程度上硬亲和力比软亲和力具有一定的优势。但在内核开发者不断的努力下,2.6内核软亲和力的缺陷已经比2.4的内核有了很大的改善。

在双核机器上,针对两线程的方案,如果将计算 apple 的线程绑定到一个 CPU 上,将计算 orange 的线程绑定到另外一个 CPU 上,效率是否会有所提高呢?

程序如下:


清单 5. CPU 亲和力
struct apple
{
	unsigned long long a;
	unsigned long long b;
};
	
struct orange
{
	int a[ORANGE_MAX_VALUE];
	int b[ORANGE_MAX_VALUE];		
};
		
inline int set_cpu(int i)
{
	CPU_ZERO(&mask);
	
	if(2 <= cpu_nums)
	{
		CPU_SET(i,&mask);
		
		if(-1 == sched_setaffinity(gettid(),sizeof(&mask),&mask))
		{
			return -1;
		}
	}
	return 0;
}

	
void* add(void* x)
{
	if(-1 == set_cpu(1))
	{
		return NULL;
	} 
		
	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
	{
		((struct apple *)x)->a += sum;
		((struct apple *)x)->b += sum;
	}	
	
	return NULL;
}
	
int main (int argc, const char * argv[]) {
		// insert code here...
	struct apple test;
	struct orange test1;
	
	cpu_nums = sysconf(_SC_NPROCESSORS_CONF);
	
	if(-1 == set_cpu(0))
	{
		return -1;
	} 
		
	pthread_create(&ThreadA,NULL,add,&test);
				
	for(index=0;index<ORANGE_MAX_VALUE;index++)
	{
		sum+=test1.a[index]+test1.b[index];
	}		
		
	pthread_join(ThreadA,NULL);
		
	return 0;
}

测量结果为:


图 5. 采用硬亲和力时间对比图(两线程)
采用硬亲和力时间对比图(两线程)

其测量结果正是我们所希望的,但花费的时间还是比单线程的多,其原因与上面分析的类似。

进一步分析不难发现,样例程序大部分时间都消耗在计算 apple 上,如果将计算 ab 的值,分布到不同的 CPU 上进行计算,同时考虑 Cache 的影响,效率是否也会有所提升呢?


图 6. 采用硬亲和力时间对比图(三线程)
采用硬亲和力时间对比图(三线程)

从时间上观察,设置亲和力的程序所花费的时间略高于采用 Cache 的三线程方案。由于考虑了 Cache 的影响,排除了一级缓存造成的瓶颈,多出的时间主要消耗在系统调用及内核上,可以通过 time 命令来验证:

#time ./unlockcachemultiprocess
    real   0m0.834s      user  0m1.644s       sys    0m0.004s
#time ./affinityunlockcacheprocess
    real   0m0.875s      user  0m1.716s       sys    0m0.008s

通过设置 CPU 亲和力来利用多核特性,为提高应用程序性能提供了捷径。同时也是一把双刃剑,如果忽略负载均衡、数据竞争等因素,效率将大打折扣,甚至带来事倍功半的结果。

在进行具体的设计过程中,需要设计良好的数据结构和算法,使其适合于应用的数据移动和处理器的性能特性。





回页首


总结

根据以上分析及实验,对所有改进方案的测试时间做一个综合对比,如下图所示:


图 7. 各方案时间对比图
各方案时间对比图

单线程原始程序平均耗时:1.049046s,最慢的不加锁三线程方案平均耗时:2.217413s,最快的三线程( Cache 为128)平均耗时:0.826674s,效率提升约26%。当然,还可以进一步优化,让效率得到更高的提升。

从上图不难得出结论:采用多核多线程并行设计方案,能有效提高性能,但如果考虑不全面,如忽略带宽、数据竞争及数据同步不当等因素,效率反而降低,程序执行越来越慢。

如果抛开本文开篇时的限制,采用上文曾提到的另外一种数据分解模型,同时结合硬亲和力对样例程序进行优化,测试时间为0.54s,效率提升了92%。

软件优化是一个贯穿整个软件开发周期,从开始设计到最终完成一直进行的连续过程。在优化前,需要找出瓶颈和热点所在。正如最伟大的 C 语言大师 Rob Pike 所说:

如果你无法断定程序会在什么地方耗费运行时间,瓶颈经常出现在意想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。

将这句话送给所有的优化人员,和大家共勉。





回页首


参考资料

  • 请参考书籍《多核程序设计技术》,了解更多关于多线程设计的理念
  • 请参考书籍《软件优化技术》,了解更多关于软件优化的技术
  • 请参考书籍《UNIX编程艺术》, 了解更多关于软件架构方面的知识
  • 参考文章《CPU Affinity》,了解更多关于CPU亲和力的信息
  • 参考文章《管理处理器的亲和性(affinity)》,了解更多关于CPU亲和力的信息





回页首


下载

描述 名字 大小 下载方法
样例代码 project.tar.gz 15KB HTTP
关于下载方法的信息


关于作者

杨小华,目前从事 Linux 内核方面的研究,喜欢捣鼓 Linux 系统,对 Linux 中断和调度系统比较了解。可以通过 normalnotebook@126.com 与他取得联系。

posted @ 2009-12-14 01:23 feedback 阅读(176) 评论(0) 编辑
    只有注册用户登录后才能阅读该文。阅读全文
posted @ 2009-03-31 10:16 feedback 阅读(64) 评论(0) 编辑