`
varsoft
  • 浏览: 2552146 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

[原创]如何改善Managed Code的Performance和Scalability系列之二:深入理解string和如何高效地使用string

阅读更多
无论你所使用的是哪种编程语言,我们都不得不承认这样一个共识:string是我们使用最为频繁的一种对象。但是string的常用性并不意味着它的简单性,而且我认为,正是由于string的频繁使用才会促使其设计人员在string的设计上花大量的功夫。所以正是这种你天天见面的string,蕴含了很多精妙的设计思想。

一个月以前我写了一篇讨论字符串的驻留(string interning)的文章,我今天将会以字符串的驻留为基础,进一步来讨论.NET中的string。string interning的基本前提是string的恒定性(immutability),即string一旦被创建将不会改变。我们就先来谈谈string的恒定性。

一、 string是恒定的(immutable)

和其他类型比较,string最为显著的一个特点就是它具有恒定不变性:我们一旦创建了一个string,在managed heap 上为他分配了一块连续的内存空间,我们将不能以任何方式对这个string进行修改使之变长、变短、改变格式。所有对这个string进行各项操作(比如调用ToUpper获得大写格式的string)而返回的string,实际上另一个重新创建的string,其本身并不会产生任何变化。

String的恒定性具有很多的好处,它首先保证了对于一个既定string的任意操作不会造成对其的改变,同时还意味着我们不用考虑操作string时候出现的线程同步的问题。在string恒定的这些好处之中,我觉得最大的好处是:它成就了字符串的驻留。

CLR通过一个内部的interning table保证了CLR只维护具有不同字符序列的string,任何具有相同字符序列的string所引用的均为同一个string对象,同一段为该string配分的内存快。字符串的驻留极大地较低了程序执行对内存的占用。

对于string的恒定性和字符串的驻留,还有一点需要特别指出的是:string的恒定性不单单是针对某一个单独的AppDomain,而是针对一个进程的。

二、 String可以跨AppDomain共享的(cross-appDomain)

我们知道,在一个托管的环境下,Appdomain是托管程序运行的一个基本单元。AppDomain为托管程序提供了良好的隔离机制,保证在同一个进程中的不同的Appdomain不可以共享相同的内存空间。在一个Appdomain创建的对象不能被另一个Appdomain直接使用,对象在AppDomain之间传递需要有一个Marshaling的过程:对象需要通过by reference或者by value的方式从一个Appdomain传递到另一个Appdomain。具体内容可以参照我的另一篇文章:用Coding证明Appdomain的隔离性

但是这里有一个特例,那就是string。Appdomain的隔离机制是为了防止一个Application的对内存空间的操作对另一个Application 内存空间的破坏。通过前面的介绍,我们已经知道了string是恒定不变的、是只读的。所以它根本不需要Appdomain的隔离机制。所以让一个恒定的、只读的string被同处于一个进程的各个Application共享是没有任何问题的。

String的这种跨AppDomain的恒定性成就了基于进程的字符串驻留:一个进程中各个Application使用的具有相同字符序列的string都是对同一段内存的引用。我们将在下面通过一个Sample来证明这一点。

三、 证明string垮AppDomain的恒定性

在写这篇文章的时候,我对如何证明string跨AppDomain的interning,想了好几天,直到我偶然地想到了为实现线程同步的lock机制。

我们知道在一个多线程的环境下,为了避免并发操作导致的数据的不一致性,我们需要对一个对象加锁来阻止该对象被另一个线程 操作。相反地,为了证明两个对象是否引用的同一个对象,我们只需要在两个线程中分别对他们加锁,如果程序执行的效果和对同一个对象加锁的情况完全一样的话,那么就可以证明这两个被加锁的对象是同一个对象。基于这样的原理我们来看看我们的Sample:

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
usingSystem.Threading;

namespaceArtech.ImmutableString
{
classProgram
{
staticvoidMain(string[]args)
{
AppDomainappDomain1
=AppDomain.CreateDomain("Artech.AppDomain1");
AppDomainappDomain2
=AppDomain.CreateDomain("Artech.AppDomain2");

MarshalByRefTypemarshalByRefObj1
=appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString","Artech.ImmutableString.MarshalByRefType")asMarshalByRefType;
MarshalByRefTypemarshalByRefObj2
=appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString","Artech.ImmutableString.MarshalByRefType")asMarshalByRefType;

marshalByRefObj1.StringLockHelper
="HelloWorld";
marshalByRefObj2.StringLockHelper
="HelloWorld";

Threadthread1
=newThread(newParameterizedThreadStart(Execute));
Threadthread2
=newThread(newParameterizedThreadStart(Execute));

thread1.Start(marshalByRefObj1);
thread2.Start(marshalByRefObj2);

Console.Read();
}


staticvoidExecute(objectobj)
{
MarshalByRefTypemarshalByRefObj
=objasMarshalByRefType;
marshalByRefObj.ExecuteWithStringLocked();
}

}


classMarshalByRefType:MarshalByRefObject
{
PrivateFields#regionPrivateFields
privatestring_stringLockHelper;
privateobject_objectLockHelper;
#endregion


PublicProperties#regionPublicProperties
publicstringStringLockHelper
{
get{return_stringLockHelper;}
set{_stringLockHelper=value;}
}


publicobjectObjectLockHelper
{
get{return_objectLockHelper;}
set{_objectLockHelper=value;}
}

#endregion


PublicMethods#regionPublicMethods
publicvoidExecuteWithStringLocked()
{
lock(this._stringLockHelper)
{
Console.WriteLine(
"Theoperationwithastringlockedisexecuted\n\tAppDomain:\t{0}\n\tTime:\t\t{1}",
AppDomain.CurrentDomain.FriendlyName,DateTime.Now);
Thread.Sleep(
10000);
}

}


publicvoidExecuteWithObjectLocked()
{
lock(this._objectLockHelper)
{
Console.WriteLine(
"Theoperationwithaobjectlockedisexecuted\n\tAppDomain:\t{0}\n\tTime:\t\t{1}",
AppDomain.CurrentDomain.FriendlyName,DateTime.Now);
Thread.Sleep(
10000);
}

}

#endregion

}

}

我们来简单地分析一下上面的coding.

我们创建了一个继承自MarshalByRefObject,因为我需要让它具有跨AppDomain传递的能力。在这个Class中定义了两个为实现线程同步的helper字段,一个是string类型的_stringLockHelper和object类型的_objectLockHelper,并为他们定义了相应的Property。此外定义了两个方法:ExecuteWithStringLocked和ExecuteWithStringLocked,他们的操作类似:在先对_stringLockHelper和_objectLockHelper加锁的前提下,输出出操作执行的AppDomain和确切时间。我们通过调用Thread.Sleep模拟10s的时间延迟。

在Main方法中,首先创建了两个AppDomain,名称分别为Artech.AppDomain1和Artech.AppDomain2。随后在这两个AppDomain中创建两个MarshalByRefType对象,并为它们的StringLockHelper属性赋上相同的值:Hello World。最后,我们创建了两个新的线程,并在它们中分别调用在两个不同AppDomain 中创建的MarshalByRefType对象的ExecuteWithStringLocked方法。我们来看看运行后的输出结果:

从上面的输出结果中可以看出,两个分别在不同线程中执行操作对应的AppDomain的name分别为Artech.AppDomain1和Artech.AppDomain2。执行的时间(确切地说是操作成功地对MarshalByRefType对象的_stringLockHelper字段进行加锁的时间)相隔10s,也就是我们在程序中定义的时间延迟。

为什么会出现这样的结果呢?我们只是对两个处于不同AppDomain的不同的MarshalByRefType对象的stringLockHelper字段进行加锁。由于我们是同时开始他们对应的线程,照理说它们之间不会有什么关联,显示出来的时间应该是相同的。唯一的解释就是:虽然这两个在不同的AppDomain中创建的对象是两个完全不同的对象,由于他们的stringLockHelper字段具有相同的字符序列,它们引用的是同一个string。这就证明了我们提出的跨AppDomain进行string interning的结论。

为了进一步印证我们的结论,我们是使两个MarshalByRefObject对象的stringLockHelper字段具有不同的值,看看结果又如何。于是我们把其中一个对象的stringLockHelper字段改为”Hello World!”(多加了一个!) 。

marshalByRefObj1.StringLockHelper="HelloWorld";
marshalByRefObj2.StringLockHelper
="HelloWorld!";

看看现在的输出结果,现在的时间是一样了。


上面我们做的是对string类型字段加锁的试验。那么我们对其他类型的对象进行加锁,又会出现怎么的情况呢?我们现在就来做这样试验:在各自的线程中调用两个对象的ExecuteWithObjectLocked方法。我们修改Execute方法和Main()。

staticvoidExecute(objectobj)
{
MarshalByRefTypemarshalByRefObj
=objasMarshalByRefType;
marshalByRefObj.ExecuteWithObjectLocked();
}

staticvoidMain(string[]args)
{
AppDomainappDomain1
=AppDomain.CreateDomain("Artech.AppDomain1");
AppDomainappDomain2
=AppDomain.CreateDomain("Artech.AppDomain2");

MarshalByRefTypemarshalByRefObj1
=appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString","Artech.ImmutableString.MarshalByRefType")asMarshalByRefType;
MarshalByRefTypemarshalByRefObj2
=appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString","Artech.ImmutableString.MarshalByRefType")asMarshalByRefType;

objectobj=newobject();
marshalByRefObj1.ObjectLockHelper
=obj;
marshalByRefObj2.ObjectLockHelper
=obj;

Threadthread1
=newThread(newParameterizedThreadStart(Execute));
Threadthread2
=newThread(newParameterizedThreadStart(Execute));

thread1.Start(marshalByRefObj1);
thread2.Start(marshalByRefObj2);

Console.Read();
}

我们先来看看运行后的输出结果:


我们发现两个时间是一样的,那么就是说两个对象的ObjectLockHelper引用的不是同一个对象。虽然上面的程序很简单,我觉得里面涉及的规程却很值得一说。我们来分析下面3段代码。

objectobj=newobject();
marshalByRefObj1.ObjectLockHelper
=obj;
marshalByRefObj2.ObjectLockHelper
=obj;

简单看起来,两个MarshalByRefObject对象的ObjectLockHelper都是引用的同一个对象obj。但是背后的情况没有那么简单。代码第一行创建了一个新的对象obj,这个对象是在当前AppDomain 中创建的。二对于当前的AppDomain来说,marshalByRefObj1和marshalByRefObj2仅仅是一个Transparent proxy而已,它们包含一个在Artech.AppDomain1和Artech.AppDomain2中创立的MarshalByRefObject对象的引用。我们为它的ObjectLockHelper复制,对于Transparent proxy对象的赋值调用会传到真正对象所在的AppDomain,由于obj是当前AppDomain的对象,它不能直接赋给另一个AppDomain的对象。所以它必须经历一个Marshaling的过程才能被传递到另外一个AppDomain。实际上当复制操作完成之后,真正的ObjectLockHelper属性对应的对象是根据原数据重建的对象,和在当前AppDomain中的对象已经没有任何的关系。所以两个MarshalByRefObject对象的ObjectLockHelper属性引用的并不是同一个对象,所以对它进行加锁对彼此不要产生任何影响。

四、 从Garbage Collection的角度来看string

我们知道在一个托管的环境下,一个对象的生命周期被GC管理和控制。一个对象只有在他不被引用的时候,GC才会对他进行垃圾回收。而对于一个string来说,它始终被interning table引用,而这个interning table是针对一个Process的,是被该Process所有AppDomain共享的,所以一个string的生命周期相对比较长,只有所有的AppDomain都不具有对该string的引用时,他才有可能被垃圾回收。

五、 从多线程的角度来看string

一方面由于string的恒定性,我们不用考虑多线程的并发操作产生的线程同步问题。另一方面由于字符串的驻留,我们在对一个string对象进行加锁操作的时候,极有可能拖慢这个Application的performance,就像我们的Sample中演示的那样。而且很有可能影响到处于同一进程的其他Application,以致造成死锁。所以我们在使用锁的时候,除非万不得已,切忌对一个string进行加锁。

六、 如何高效地使用string

下面简单介绍一些高效地使用string的一些小的建议:

1.尽量使用字符串(literal string)相加来代替字符串变量和字符创相加,因为这样可以使用现有的string操作指令进行操作和利用字符串驻留。

比如:

strings="abc"+"def";

优于

strings="abc";
s
=s+"def";

2.在需要的时候使用StringBuilder对string作频繁的操作:

由于string的恒定性,在我们对一个string进行某些操作的时候,比如调用ToUpper()或者ToLower()把某个string每个字符转化成大写或者小写;调用SubString()取子串;会创建一个新的string,有时候会创建一些新的临时string。这样的操作会增加内存的压力。所有在对string作频繁操作的情况下,我们会考虑使用StringBuilder来高效地操作string。StringBuilder之所以能对string操作带来更好的performance,是因为在它的内部维护一个字符数组,而不是一个string来避免string操作带来的新的string的创建。

StringBuilder是一个很好的字符累加器,我们应该充分地利用这一个功能:

StringBuildersb=newStringBuilder();
sb.Append(str1
+str2);

最好写成

StringBuildersb=newStringBuilder();
sb.Append(str1);
sb.Append(str2);

避免创建一个新的临时string来保存str1 + str2。

再比如下面的Code

StringBuildersb=newStringBuilder();
sb.Append(WorkOnString1());
sb.Append(WorkOnString2());
sb.Append(WorkOnString3());

最好写好吧WorkOnString1,WorkOnString2,WorkOnString3定义成:

WorkOnString1(StringBuildersb)
WorkOnString2(StringBuildersb)
WorkOnString3(StringBuildersb)

3.高效地进行string的比较操作

我们知道,对象之间的比较有比较Value和比较Reference之说。一般地对Reference进行比较的速度最快。对于string,在字符串驻留的前提下,我们可以把对Value的比较用Reference的比较来代替从而会的Performance的提升。

此外,对于忽略大小写的比较,我们最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。也就是说:

if(str1.ToLower()==str2.ToLower())

最好写成

If(string.Compare(str1,str2,true))
分享到:
评论

相关推荐

    DirectX for Managed Code

    DirectX for Managed Code是微软为.NET开发者提供的一套API,旨在让C#和其他.NET语言的程序员能够充分利用DirectX的功能来创建高性能的图形和多媒体应用程序。DirectX是一个包含多个子组件的集合,主要用于游戏开发...

    Chapter 5 - Improving Managed Code Performance.pdf

    9. **值类型和引用类型**:理解.NET中的值类型和引用类型之间的差异对于编写高效的代码非常重要。文档阐释了值类型和引用类型之间的转换和性能影响。 10. **装箱和取消装箱详解**:装箱(Boxing)和取消装箱...

    Managed.Code.Rootkits.2011

    《Managed.Code.Rootkits.2011》是关于.NET Rootkits的一个专题,深入探讨了在.NET框架下创建和检测Rootkit的技术。Rootkit是一种恶意软件工具,它隐藏在操作系统内部,使得攻击者能够秘密地控制受感染的系统,同时...

    managed code vs unmanaged code

    在软件开发领域,代码根据其运行时的特性可以分为管理代码(Managed Code)和非管理代码(Unmanaged Code)。这两种类型的代码有着本质的区别,并且适用于不同的场景和技术栈。下面将详细探讨它们之间的差异。 ####...

    深入Managed DirectX 9文档

    **深入Managed DirectX 9 文档** Managed DirectX 9(MDX)是微软为.NET Framework提供的一套编程接口,...通过深入理解和实践这些知识点,开发者可以更好地驾驭C#和Managed DirectX 9.0,创造出更具吸引力的作品。

    深入Managed DirectX 9

    为了深入学习Managed DirectX 9,开发者应该熟悉DirectX的渲染管线,理解从顶点到像素的转换过程,包括顶点着色器和像素着色器的使用。同时,学习如何有效地管理资源,如纹理和缓冲,以及如何利用DirectX提供的高级...

    深入managed directx9

    《深入Managed DirectX 9》是一本专为游戏开发者和图形编程爱好者量身打造的技术书籍,主要探讨了如何利用Microsoft的Managed DirectX 9框架进行高效、高质量的3D图形编程。Managed DirectX是.NET Framework的一部分...

    高清彩版 Writing High-Performance NET Code(2nd))

    本书旨在帮助读者深入理解如何编写高性能的 .NET 应用程序。通过丰富的示例代码、实用工具和技术讲解,作者详细介绍了 .NET 平台的核心组件以及如何利用这些组件来优化应用程序的性能。 #### 选择管理代码的原因 ...

    DirectX for Manager Code

    同时,注意学习和理解DirectX的编程模型,包括设备创建、资源管理、渲染流程等,这对于有效地使用这个库至关重要。 总的来说,"DirectX for Managed Code"是一个让.NET开发者能够轻松利用DirectX功能的工具,尽管它...

    深入Managed DirectX9

    Managed DirectX 9是一种微软提供的API(应用程序接口),用于在Windows平台上进行游戏开发和多媒体应用的编程。这个框架允许开发者直接与硬件交互,充分利用图形处理器(GPU)的能力,提供高效的3D渲染、音频处理...

    深入Managed_DirectX9.0 源代码

    这个压缩包文件"深入Managed_DirectX9源代码"显然包含了对这一技术的深入解析和示例源代码,非常适合想要学习或提升在Managed DirectX 9.0开发能力的程序员。 Managed DirectX 9.0主要包含以下几个核心组件: 1. *...

    关于托管C++和非托管C++各种字符串类型的转换

    - **非托管C++中的字符串**:通常使用`std::string`类型,它是C++标准库的一部分,支持ASCII编码和部分Unicode扩展,通过模板机制实现高效内存管理和操作。 #### 字符串类型转换方法 ##### 1. 将`std::string`转换...

    Managed IO Completion Ports.docx

    ket. 2.Associate the listen socket with an IOCP. 3.When a new client connects, accept the connection and create a new ...正确使用Managed IOCP,能帮助你的应用程序在处理大量并发连接时保持高性能和低延迟。

    深入Managed DirectX9.doc

    总的来说,"深入Managed DirectX9.doc"这份文档将深入讲解这些概念和技术,为开发者提供详尽的指南,帮助他们熟练掌握Managed DirectX9的使用,创作出高效且富于表现力的图形应用程序。通过学习和实践,开发者可以...

    Managed Directx 9 Kick Start - Graphics And Game Programming Source Code

    "Managed DirectX 9 Kick Start - Graphics And Game Programming" 是一本专注于教授如何使用Managed DirectX 9进行图形与游戏编程的资源,其中包含了源代码和C#语言的示例。 本书的核心目标是帮助开发者快速掌握...

    myBatis系列之七:事务管理

    本文将深入探讨“myBatis系列之七:事务管理”这一主题,结合源码分析和实用工具,帮助你理解MyBatis中的事务处理机制。 首先,事务在数据库操作中扮演着至关重要的角色,它保证了数据的一致性和完整性。在MyBatis...

    Mixing Native and Managed Types in C++.

    在C++编程中,混合使用原生(Native)和托管(Managed)类型是一个常见的需求,尤其是在处理跨平台或与现有代码库集成时。C++/CLI(C++ Common Language Infrastructure)是微软为.NET Framework提供的一种语言扩展...

    在ASP.NET 2.0中操作数据:用Managed Code创建存储过程和用户自定义函数(上部分)

    在ASP.NET 2.0中操作数据:用Managed Code创建存储过程和用户自定义函数(上部分)

Global site tag (gtag.js) - Google Analytics