.NET程序品质优化基本要领

鸣谢

首先,我要为这段代码在Linux系统上做的精准分析感谢Rutenberg。

多亏了Wikipedia,让“在指尖的信息”的梦想得以实现。

最后,感谢你花时间阅读这篇文章。希望你喜欢它:不论如何,请分享您的意见。

7、stack

class studnet{public:    int age;    void printage(){        cout<<age<<"t";    }};int main() {    stack<int> st;    st.push;    st.push;    st.push;    st.push;    while (!st.empty{        int temp = st.top();        cout<<temp<<"t";        st.pop();    }    cout<<endl;    stack<studnet> st2;    for (int i = 0; i < 5; ++i) {        studnet temp2;        temp2.age=i+20;        st2.push;    }    while (!st2.empty{        studnet temp2 = st2.top();        temp2.printage();        st2.pop();    }    return 0;}/*输出4       3       2       124      23      22      21      20*/

例6 缓存异步方法

Visual Studio IDE
的特性在很大程度上建立在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍能够保持Visual   
Stuido能够响应。下面是获取语法树的第一个版本的代码:

 

[js] view
plaincopyprint?

  1. class Parser   
  2. {  
  3.     /*…*/   
  4.     public SyntaxTree Syntax  
  5.     {   
  6.         get;   
  7.     }   
  8.       
  9.     public Task ParseSourceCode()   
  10.     {  
  11.         /*…*/   
  12.     }   
  13. }  
  14. class Compilation   
  15. {   
  16.     /*…*/   
  17.     public async Task<SyntaxTree> GetSyntaxTreeAsync()   
  18.     {   
  19.         var parser = new Parser(); // allocation   
  20.         await parser.ParseSourceCode(); // expensive   
  21.         return parser.Syntax;  
  22.     }   
  23. }  

    class Parser
    {

    /*...*/ 
    public SyntaxTree Syntax
    { 
        get; 
    } 
    
    public Task ParseSourceCode() 
    {
        /*...*/ 
    } 
    

    }
    class Compilation
    {

    /*...*/ 
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax;
    } 
    

    }

 

可以看到调用GetSyntaxTreeAsync()
方法会实例化一个Parser对象,解析代码,然后返回一个Task<SyntaxTree>对象。最耗性能的地方在为Parser实例分配内存并解析代码。方法中返回一个Task对象,因此调用者可以await解析工作,然后释放UI线程使得可以响应用户的输入。

由于Visual Studio的一些特性可能需要多次获取相同的语法树,
所以通常可能会缓存解析结果来节省时间和内存分配,但是下面的代码可能会导致内存分配:

 

[js] view
plaincopyprint?

  1. class Compilation   
  2. { /*…*/  
  3.     private SyntaxTree cachedResult;  
  4.     public async Task<SyntaxTree> GetSyntaxTreeAsync()   
  5.     {   
  6.         if (this.cachedResult == null)   
  7.         {   
  8.             var parser = new Parser(); // allocation   
  9.             await parser.ParseSourceCode(); // expensive   
  10.             this.cachedResult = parser.Syntax;   
  11.         }   
  12.         return this.cachedResult;  
  13.     }  
  14. }  

    class Compilation
    { //

    private SyntaxTree cachedResult;
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        if (this.cachedResult == null) 
        { 
            var parser = new Parser(); // allocation 
            await parser.ParseSourceCode(); // expensive 
            this.cachedResult = parser.Syntax; 
        } 
        return this.cachedResult;
    }
    

    }

 

代码中有一个SynataxTree类型的名为cachedResult的字段。当该字段为空的时候,GetSyntaxTreeAsync()执行,然后将结果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree对象。问题在于,当有一个类型为Task<SyntaxTree>
类型的async异步方法时,想要返回SyntaxTree的值,编译器会生出代码来分配一个Task来保存执行结果(通过使用Task<SyntaxTree>.FromResult())。Task会标记为完成,然后结果立马返回。分配Task对象来存储执行的结果这个动作调用非常频繁,因此修复该分配问题能够极大提高应用程序响应性。

解决方法:

要移除保存完成了执行任务的分配,可以缓存Task对象来保存完成的结果。

 

[js] view
plaincopyprint?

  1. class Compilation   
  2. { /*…*/  
  3.     private Task<SyntaxTree> cachedResult;  
  4.     public Task<SyntaxTree> GetSyntaxTreeAsync()   
  5.     {   
  6.         return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync());   
  7.     }  
  8.     private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync()   
  9.     {  
  10.         var parser = new Parser(); // allocation   
  11.         await parser.ParseSourceCode(); // expensive   
  12.         return parser.Syntax;   
  13.     }   
  14. }  

    class Compilation
    { //

    private Task<SyntaxTree> cachedResult;
    public Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync()); 
    }
    private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync() 
    {
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax; 
    } 
    

    }

 

代码将cachedResult 类型改为了Task<SyntaxTree>
并且引入了async帮助函数来保存原始代码中的GetSyntaxTreeAsync()函数。GetSyntaxTreeAsync函数现在使用    
null操作符,来表示当cachedResult不为空时直接返回,为空时GetSyntaxTreeAsync调用GetSyntaxTreeUncachedAsync()然后缓存结果。注意GetSyntaxTreeAsync并没有await调用GetSyntaxTreeUncachedAsync。没有使用await意味着当GetSyntaxTreeUncachedAsync返回Task类型时,GetSyntaxTreeAsync
也立即返回Task,       
现在缓存的是Task,因此在返回缓存结果的时候没有额外的内存分配。

常见的内存分配以及例子

这部分的例子虽然背后关于内存分配的地方很少。但是,如果一个大的应用程序执行足够多的这些小的会导致内存分配的表达式,那么这些表达式会导致几百M,甚至几G的内存分配。比如,在性能测试团队把问题定位到输入场景之前,一分钟的测试模拟开发者在编译器里面编写代码会分配几G的内存。

现在稍等一下!

你可能会问:你是在试着说服我们提前优化吗?

不是的。我赞同提前优化是糟糕的。这种优化并不是提前的:是及时的。这是基于经验的优化:我发现自己过去一直在和这种特殊的怪胎搏斗。基于经验的优化(不在同一个地方摔倒两次)并不是提前优化。

当我们优化性能时,“惯犯”会包括磁盘I-O操作、网络访问(数据库、web服务)和内层循环;对于这些,我们应该添加内存分配和性能糟糕的
Keyser Söze。

一、容器

例2 枚举类型的装箱

下面的这个例子是导致新的C#
和VB编译器由于频繁的使用枚举类型,特别是在Dictionary中做查找操作时分配了大量内存的原因。

 

[js] view
plaincopyprint?

  1. public enum Color { Red, Green, Blue }  
  2. public class BoxingExample  
  3. {  
  4.     private string name;  
  5.     private Color color;  
  6.     public override int GetHashCode()  
  7.     {  
  8.         return name.GetHashCode() ^ color.GetHashCode();  
  9.     }  
  10. }  

    public enum Color { Red, Green, Blue }
    public class BoxingExample
    {

    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
    

    }

 

问题非常隐蔽,PerfView会告诉你enmu.GetHashCode()由于内部实现的原因产生了装箱操作,该方法会在底层枚举类型的表现形式上进行装箱,如果仔细看PerfView,会看到每次调用GetHashCode会产生两次装箱操作。编译器插入一次,.NET   
Framework插入另外一次。

解决方法:

通过在调用GetHashCode的时候将枚举的底层表现形式进行强制类型转换就可以避免这一装箱操作。

 

[js] view
plaincopyprint?

  1. ((int)color).GetHashCode()  

    ((int)color).GetHashCode()

 

另一个使用枚举类型经常产生装箱的操作时enum.HasFlag。传给HasFlag的参数必须进行装箱,在大多数情况下,反复调用HasFlag通过位运算测试非常简单和不需要分配内存。

要牢记基本要领第一条,不要过早优化。并且不要过早的开始重写所有代码。
需要注意到这些装箱的耗费,只有在通过工具找到并且定位到最主要问题所在再开始修改代码。

字典

在很多应用程序中,Dictionary用的很广,虽然字非常方便和高校,但是经常会使用不当。在Visual
Studio以及新的编译器中,使用性能分析工具发现,许多dictionay只包含有一个元素或者干脆是空的。一个空的Dictionay结构内部会有10个字段在x86机器上的托管堆上会占据48个字节。当需要在做映射或者关联数据结构需要事先常量时间查找的时候,字典非常有用。但是当只有几个元素,使用字典就会浪费大量内存空间。相反,我们可以使用List<KeyValuePair<K,V>>结构来实现便利,对于少量元素来说,同样高校。如果仅仅使用字典来加载数据,然后读取数据,那么使用一个具有N(log(N))的查找效率的有序数组,在速度上也会很快,当然这些都取决于的元素的个数。

背景

如果google一下“C++
StringBuilder”,你会得到不少答案。有些会建议(你)使用std::accumulate,这可以完成几乎所有你要实现的:

#include <iostream>// for std::cout, std::endl
#include <string>  // for std::string
#include <vector>  // for std::vector
#include <numeric> // for std::accumulate
int main()
{
    using namespace std;
    vector<string> vec = { "hello", " ", "world" };
    string s = accumulate(vec.begin(), vec.end(), s);
    cout << s << endl; // prints 'hello world' to standard output. 
    return 0;
}

目前为止一切都好:当你有超过几个字符串连接时,问题就出现了,并且内存再分配也开始积累。

std::string在函数reserver()中为解决方案提供基础。这也正是我们的意图所在:一次分配,随意连接。

字符串连接可能会因为繁重、迟钝的工具而严重影响性能。由于上次存在的隐患,这个特殊的怪胎给我制造麻烦,我便放弃了Indigo(我想尝试一些C++11里的令人耳目一新的特性),并写了一个StringBuilder类的部分实现:

// Subset of http://msdn.microsoft.com/en-us/library/system.text.stringbuilder.aspx
template <typename chr>
class StringBuilder {
    typedef std::basic_string<chr> string_t;
    typedef std::list<string_t> container_t; // Reasons not to use vector below. 
    typedef typename string_t::size_type size_type; // Reuse the size type in the string.
    container_t m_Data;
    size_type m_totalSize;
    void append(const string_t &src) {
        m_Data.push_back(src);
        m_totalSize += src.size();
    }
    // No copy constructor, no assignement.
    StringBuilder(const StringBuilder &);
    StringBuilder & operator = (const StringBuilder &);
public:
    StringBuilder(const string_t &src) {
        if (!src.empty()) {
            m_Data.push_back(src);
        }
        m_totalSize = src.size();
    }
    StringBuilder() {
        m_totalSize = 0;
    }
    // TODO: Constructor that takes an array of strings.

    StringBuilder & Append(const string_t &src) {
        append(src);
        return *this; // allow chaining.
    }
        // This one lets you add any STL container to the string builder. 
    template<class inputIterator>
    StringBuilder & Add(const inputIterator &first, const inputIterator &afterLast) {
        // std::for_each and a lambda look like overkill here.
                // <b>Not</b> using std::copy, since we want to update m_totalSize too.
        for (inputIterator f = first; f != afterLast; ++f) {
            append(*f);
        }
        return *this; // allow chaining.
    }
    StringBuilder & AppendLine(const string_t &src) {
        static chr lineFeed[] { 10, 0 }; // C++ 11. Feel the love!
        m_Data.push_back(src + lineFeed);
        m_totalSize += 1 + src.size();
        return *this; // allow chaining.
    }
    StringBuilder & AppendLine() {
        static chr lineFeed[] { 10, 0 }; 
        m_Data.push_back(lineFeed);
        ++m_totalSize;
        return *this; // allow chaining.
    }

    // TODO: AppendFormat implementation. Not relevant for the article. 

    // Like C# StringBuilder.ToString()
    // Note the use of reserve() to avoid reallocations. 
    string_t ToString() const {
        string_t result;
        // The whole point of the exercise!
        // If the container has a lot of strings, reallocation (each time the result grows) will take a serious toll,
        // both in performance and chances of failure.
        // I measured (in code I cannot publish) fractions of a second using 'reserve', and almost two minutes using +=.
        result.reserve(m_totalSize + 1);
    //  result = std::accumulate(m_Data.begin(), m_Data.end(), result); // This would lose the advantage of 'reserve'
        for (auto iter = m_Data.begin(); iter != m_Data.end(); ++iter) { 
            result += *iter;
        }
        return result;
    }

    // like javascript Array.join()
    string_t Join(const string_t &delim) const {
        if (delim.empty()) {
            return ToString();
        }
        string_t result;
        if (m_Data.empty()) {
            return result;
        }
        // Hope we don't overflow the size type.
        size_type st = (delim.size() * (m_Data.size() - 1)) + m_totalSize + 1;
        result.reserve(st);
                // If you need reasons to love C++11, here is one.
        struct adder {
            string_t m_Joiner;
            adder(const string_t &s): m_Joiner(s) {
                // This constructor is NOT empty.
            }
                        // This functor runs under accumulate() without reallocations, if 'l' has reserved enough memory. 
            string_t operator()(string_t &l, const string_t &r) {
                l += m_Joiner;
                l += r;
                return l;
            }
        } adr(delim);
        auto iter = m_Data.begin(); 
                // Skip the delimiter before the first element in the container.
        result += *iter; 
        return std::accumulate(++iter, m_Data.end(), result, adr);
    }

}; // class StringBuilder

拷贝替换算法

结论

在大的系统,或者或者需要处理大量数据的系统中,我们需要关注产生性能瓶颈症状,这些问题再规模上会影响app的响应性,如装箱操作、字符串操作、LINQ和Lambda表达式、缓存async方法、缓存缺少大小限制以及良好的资源释放策略、使用Dictionay不当、以及到处传递结构体等。在优化我们的应用程序的时候,需要时刻注意之前提到过的四点:

  1. 不要进行过早优化——在定位和发现问题之后再进行调优。
  2. 专业测试不会说谎——没有评测,便是猜测。
  3. 好工具很重要。——下载        
    PerfView,然后去看使用教程。
  4. 内存分配决定app的响应性。——这也是新的编译器性能团队花的时间最多的地方。

有趣的部分

函数ToString()使用std::string::reserve()来实现最小化再分配。下面你可以看到一个性能测试的结果。

函数join()使用std::accumulate(),和一个已经为首个操作数预留内存的自定义函数。

你可能会问,为什么StringBuilder::m_Data用std::list而不是std::vector?除非你有一个用其他容器的好理由,通常都是使用std::vector。

好吧,我(这样做)有两个原因:

1.
字符串总是会附加到一个容器的末尾。std::list允许在不需要内存再分配的情况下这样做;因为vector是使用一个连续的内存块实现的,每用一个就可能导致内存再分配。

2.
std::list对顺序存取相当有利,而且在m_Data上所做的唯一存取操作也是顺序的。

你可以建议同时测试这两种实现的性能和内存占用情况,然后选择其中一个。

10、string

要领一:不要过早优化

编写代码比想象中的要复杂的多,代码需要维护,调试及优化性能。
一个有经验的程序员,通常会对自然而然的提出解决问题的方法并编写高效的代码。
但是有时候也可能会陷入过早优化代码的问题中。比如,有时候使用一个简单的数组就够了,非要优化成使用哈希表,有时候简单的重新计算一下可以,非要使用复杂的可能导致内存泄漏的缓存。发现问题时,应该首先测试性能问题然后再分析代码。

Aync异步

接下来的例子展示了当我们试图缓存一部方法返回值时的一个普遍问题:

性能评估

为了测试性能,我从Wikipedia获取一个网页,并将其中一部分内容写死到一个string的vector中。

随后,我编写两个测试函数,第一个在两个循环中使用标准函数clock()并调用std::accumulate()和StringBuilder::ToString(),然后打印结果。

void TestPerformance(const StringBuilder<wchar_t> &tested, const std::vector<std::wstring> &tested2) {
    const int loops = 500;
    clock_t start = clock(); // Give up some accuracy in exchange for platform independence.
    for (int i = 0; i < loops; ++i) {
        std::wstring accumulator;
        std::accumulate(tested2.begin(), tested2.end(), accumulator);
    }
    double secsAccumulate = (double) (clock() - start) / CLOCKS_PER_SEC;

    start = clock();
    for (int i = 0; i < loops; ++i) {
        std::wstring result2 = tested.ToString();
    }
    double secsBuilder = (double) (clock() - start) / CLOCKS_PER_SEC;
    using std::cout;
    using std::endl;
    cout << "Accumulate took " << secsAccumulate << " seconds, and ToString() took " << secsBuilder << " seconds."
            << " The relative speed improvement was " << ((secsAccumulate / secsBuilder) - 1) * 100 << "%"
            << endl;
}

第二个则使用更精确的Posix函数clock_gettime(),并测试StringBuilder::Join()。

#ifdef __USE_POSIX199309

// Thanks to <a href="http://www.guyrutenberg.com/2007/09/22/profiling-code-using-clock_gettime/">Guy Rutenberg</a>.
timespec diff(timespec start, timespec end)
{
    timespec temp;
    if ((end.tv_nsec-start.tv_nsec)<0) {
        temp.tv_sec = end.tv_sec-start.tv_sec-1;
        temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
    } else {
        temp.tv_sec = end.tv_sec-start.tv_sec;
        temp.tv_nsec = end.tv_nsec-start.tv_nsec;
    }
    return temp;
}

void AccurateTestPerformance(const StringBuilder<wchar_t> &tested, const std::vector<std::wstring> &tested2) {
    const int loops = 500;
    timespec time1, time2;
    // Don't forget to add -lrt to the g++ linker command line.
    ////////////////
    // Test std::accumulate()
    ////////////////
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
    for (int i = 0; i < loops; ++i) {
        std::wstring accumulator;
        std::accumulate(tested2.begin(), tested2.end(), accumulator);
    }
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
    using std::cout;
    using std::endl;
    timespec tsAccumulate =diff(time1,time2);
    cout << tsAccumulate.tv_sec << ":" <<  tsAccumulate.tv_nsec << endl;
    ////////////////
    // Test ToString()
    ////////////////
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
    for (int i = 0; i < loops; ++i) {
        std::wstring result2 = tested.ToString();
    }
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
    timespec tsToString =diff(time1,time2);
    cout << tsToString.tv_sec << ":" << tsToString.tv_nsec << endl;
    ////////////////
    // Test join()
    ////////////////
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
    for (int i = 0; i < loops; ++i) {
        std::wstring result3 = tested.Join(L",");
    }
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
    timespec tsJoin =diff(time1,time2);
    cout << tsJoin.tv_sec << ":" << tsJoin.tv_nsec << endl;

    ////////////////
    // Show results
    ////////////////
    double secsAccumulate = tsAccumulate.tv_sec + tsAccumulate.tv_nsec / 1000000000.0;
    double secsBuilder = tsToString.tv_sec + tsToString.tv_nsec / 1000000000.0;
        double secsJoin = tsJoin.tv_sec + tsJoin.tv_nsec / 1000000000.0;
    cout << "Accurate performance test:" << endl << "    Accumulate took " << secsAccumulate << " seconds, and ToString() took " << secsBuilder << " seconds." << endl
            << "    The relative speed improvement was " << ((secsAccumulate / secsBuilder) - 1) * 100 << "%" << endl <<
             "     Join took " << secsJoin << " seconds."
            << endl;
}
#endif // def __USE_POSIX199309

最后,通过一个main函数调用以上实现的两个函数,将结果显示在控制台,然后执行性能测试:一个用于调试配置。

图片 1

t另一个用于发行版本:

图片 2

看到这百分比没?垃圾邮件的发送量都不能达到这个级别!

reverse

int main(){    vector<int> vector1;    for (int i = 0; i < 10; ++i) {        vector1.push_back;    }    for_each(vector1.begin(),vector1.end(),[]{cout<<s<<" ";});    reverse(vector1.begin(),vector1.end;    cout<<"n-----------n";    for_each(vector1.begin(),vector1.end(),[]{cout<<s<<" ";});    return  0;}/*0 1 2 3 4 5 6 7 8 9-----------9 8 7 6 5 4 3 2 1 0*/

要领四:所有的都与内存分配相关

你可能会想,编写响应及时的基于.NET的应用程序关键在于采用好的算法,比如使用快速排序替代冒泡排序,但是实际情况并不是这样。编写一个响应良好的app的最大因素在于内存分配,特别是当app非常大或者处理大量数据的时候。

在使用新的编译器API开发响应良好的IDE的实践中,大部分工作都花在了如何避免开辟内存以及管理缓存策略。PerfView追踪显示新的C#
和VB编译器的性能基本上和CPU的性能瓶颈没有关系。编译器在读入成百上千甚至上万行代码,读入元数据活着产生编译好的代码,这些操作其实都是I/O   
bound
密集型。UI线程的延迟几乎全部都是由于垃圾回收导致的。.NET框架对垃圾回收的性能已经进行过高度优化,他能够在应用程序代码执行的时候并行的执行垃圾回收的大部分操作。但是,单个内存分配操作有可能会触发一次昂贵的垃圾回收操作,这样GC会暂时挂起所有线程来进行垃圾回收(比如    
Generation
2型的垃圾回收.aspx))

为什么来自新的编译器的性能优化经验也适用于您的应用程序

微软使用托管代码重写了C#和Visual
Basic的编译器,并提供了一些列新的API来进行代码建模和分析、开发编译工具,使得Visual
Studio具有更加丰富的代码感知的编程体验。重写编译器,并且在新的编译器上开发Visual
Studio的经验使得我们获得了非常有用的性能优化经验,这些经验也能用于大型的.NET应用,或者一些需要处理大量数据的APP上。你不需要了解编译器,也能够从C#编译器的例子中得出这些见解。

Visual
Studio使用了编译器的API来实现了强大的智能感知(Intellisense)功能,如代码关键字着色,语法填充列表,错误波浪线提示,参数提示,代码问题及修改建议等,这些功能深受开发者欢迎。Visual
Studio在开发者输入或者修改代码的时候,会动态的编译代码来获得对代码的分析和提示。

当用户和App进行交互的时候,通常希望软件具有好的响应性。输入或者执行命令的时候,应用程序界面不应该被阻塞。帮助或者提示能够迅速显示出来或者当用户继续输入的时候停止提示。现在的App应该避免在执行长时间计算的时候阻塞UI线程从而让用户感觉程序不够流畅。

想了解更多关于新的编译器的信息,可以访问 .NET Compiler Platform
(“Roslyn”)

介绍

经常出现客户端打电话抱怨说:你们的程序慢如蜗牛。你开始检查可能的疑点:文件IO,数据库访问速度,甚至查看web服务。
但是这些可能的疑点都很正常,一点问题都没有。

你使用最顺手的性能分析工具分析,发现瓶颈在于一个小函数,这个函数的作用是将一个长的字符串链表写到一文件中。

你对这个函数做了如下优化:将所有的小字符串连接成一个长的字符串,执行一次文件写入操作,避免成千上万次的小字符串写文件操作。

这个优化只做对了一半。

你先测试大字符串写文件的速度,发现快如闪电。然后你再测试所有字符串拼接的速度。

好几年。

怎么回事?你会怎么克服这个问题呢?

你或许知道.net程序员可以使用StringBuilder来解决此问题。这也是本文的起点。

vector复制vector

实现从vector1复制到vector2,实验证明,四种方式都实现了内存上的复制

vectorvector2;

vectorvector3(vector1.begin(),vector1.end;

vectorvector4=vector1;

vectorvector5;
​ vector5.resize(vector1.size;
​ copy(vector1.begin(),vector1.end(),vector5.begin;

方法四,使用std::copy(),前一定对新的vector进行resize(),否则异常

例4 StringBuilder

本例中使用StringBuilder。下面的函数用来产生泛型类型的全名:

 

[js] view
plaincopyprint?

  1. public class Example   
  2. {   
  3.     // Constructs a name like “SomeType<T1, T2, T3>”   
  4.     public string GenerateFullTypeName(string name, int arity)   
  5.     {   
  6.         StringBuilder sb = new StringBuilder();  
  7.         sb.Append(name);  
  8.         if (arity != 0)  
  9.         {   
  10.             sb.Append(“<“);  
  11.             for (int i = 1; i < arity; i++)  
  12.             {  
  13.                 sb.Append(“T”); sb.Append(i.ToString()); sb.Append(“, “);  
  14.             }   
  15.             sb.Append(“T”); sb.Append(i.ToString()); sb.Append(“>”);  
  16.         }  
  17.         return sb.ToString();   
  18.     }  
  19. }  

    public class Example
    {

    // Constructs a name like "SomeType<T1, T2, T3>" 
    public string GenerateFullTypeName(string name, int arity) 
    { 
        StringBuilder sb = new StringBuilder();
        sb.Append(name);
        if (arity != 0)
        { 
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            } 
            sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
        }
        return sb.ToString(); 
    }
    

    }

 

注意力集中到StringBuilder实例的创建上来。代码中调用sb.ToString()会导致一次内存分配。在StringBuilder中的内部实现也会导致内部内存分配,但是我们如果想要获取到string类型的结果化,这些分配无法避免。

解决方法:

要解决StringBuilder对象的分配就使用缓存。即使缓存一个可能被随时丢弃的单个实例对象也能够显著的提高程序性能。下面是该函数的新的实现。除了下面两行代码,其他代码均相同

 

[js] view
plaincopyprint?

  1. // Constructs a name like “Foo<T1, T2, T3>”   
  2. public string GenerateFullTypeName(string name, int arity)  
  3. {  
  4.     StringBuilder sb = AcquireBuilder(); /* Use sb as before */   
  5.     return GetStringAndReleaseBuilder(sb);  
  6. }  

    // Constructs a name like “Foo
    public string GenerateFullTypeName(string name, int arity)
    {

    StringBuilder sb = AcquireBuilder(); /* Use sb as before */ 
    return GetStringAndReleaseBuilder(sb);
    

    }

 

关键部分在于新的
AcquireBuilder()GetStringAndReleaseBuilder()方法:

 

[js] view
plaincopyprint?

  1. [ThreadStatic]  
  2. private static StringBuilder cachedStringBuilder;  
  3.   
  4. private static StringBuilder AcquireBuilder()  
  5. {  
  6.     StringBuilder result = cachedStringBuilder;  
  7.     if (result == null)  
  8.     {  
  9.         return new StringBuilder();  
  10.     }   
  11.     result.Clear();   
  12.     cachedStringBuilder = null;   
  13.     return result;  
  14. }  
  15.   
  16. private static string GetStringAndReleaseBuilder(StringBuilder sb)  
  17. {  
  18.     string result = sb.ToString();   
  19.     cachedStringBuilder = sb;   
  20.     return result;  
  21. }  

    [ThreadStatic]
    private static StringBuilder cachedStringBuilder;

    private static StringBuilder AcquireBuilder()
    {

    StringBuilder result = cachedStringBuilder;
    if (result == null)
    {
        return new StringBuilder();
    } 
    result.Clear(); 
    cachedStringBuilder = null; 
    return result;
    

    }

    private static string GetStringAndReleaseBuilder(StringBuilder sb)
    {

    string result = sb.ToString(); 
    cachedStringBuilder = sb; 
    return result;
    

    }

 

上面方法实现中使用了    
thread-static.aspx)字段来缓存StringBuilder对象,这是由于新的编译器使用了多线程的原因。很可能会忘掉这个ThreadStatic声明。Thread-static字符为每个执行这部分的代码的线程保留一个唯一的实例。

如果已经有了一个实例,那么AcquireBuilder()方法直接返回该缓存的实例,在清空后,将该字段或者缓存设置为null。否则AcquireBuilder()创建一个新的实例并返回,然后将字段和cache设置为null    

当我们对StringBuilder处理完成之后,调用GetStringAndReleaseBuilder()方法即可获取string结果。然后将StringBuilder保存到字段中或者缓存起来,然后返回结果。这段代码很可能重复执行,从而创建多个StringBuilder对象,虽然很少会发生。代码中仅保存最后被释放的那个StringBuilder对象来留作后用。新的编译器中,这种简单的的缓存策略极大地减少了不必要的内存分配。.NET   
Framework 和    
MSBuild中的部分模块也使用了类似的技术来提升性能。

 

 

 

 

 

2,

本文分享了性能优化的一些建议和思考,比如不要过早优化、好工具很重要、性能的关键,在于内存分配等。开发者不要盲目的没有根据的优化,首先定位和查找到造成产生性能问题的原因点最重要。

 

本文提供了一些性能优化的建议,这些经验来自于使用托管代码重写C# 和
VB编译器,并以编写C#
编译器中的一些真实场景作为例子来展示这些优化经验。.NET
平台开发应用程序具有极高的生产力。.NET
平台上强大安全的编程语言以及丰富的类库,使得开发应用变得卓有成效。但是能力越大责任越大。我们应该使用.NET框架的强大能力,但同时如果我们需要处理大量的数据比如文件或者数据库也需要准备对我们的代码进行调优。

代码使用

在使用这段代码前,
考虑使用ostring流。正如你在下面看到Jeff先生评论的一样,它比这篇文章中的代码更快些。

你可能想使用这段代码,如果:

  • 你正在编写由具有C#经验的程序员维护的代码,并且你想提供一个他们所熟悉接口的代码。
  • 你正在编写将来会转换成.net的、你想指出一个可能路径的代码。
  • 由于某些原因,你不想包含<sstream>。几年之后,一些流的IO实现变得很繁琐,而且现在的代码仍然不能完全摆脱他们的干扰。

要使用这段代码,只有按照main函数实现的那样就可以了:创建一个StringBuilder的实例,用Append()、AppendLine()和Add()给它赋值,然后调用ToString函数检索结果。

就像下面这样:

int main() {
    ////////////////////////////////////
    // 8-bit characters (ANSI)
    ////////////////////////////////////
    StringBuilder<char> ansi;
    ansi.Append("Hello").Append(" ").AppendLine("World");
    std::cout << ansi.ToString();

    ////////////////////////////////////
    // Wide characters (Unicode)
    ////////////////////////////////////
    // http://en.wikipedia.org/wiki/Cargo_cult
    std::vector<std::wstring> cargoCult
    {
        L"A", L" cargo", L" cult", L" is", L" a", L" kind", L" of", L" Melanesian", L" millenarian", L" movement",
// many more lines here...
L" applied", L" retroactively", L" to", L" movements", L" in", L" a", L" much", L" earlier", L" era.n"
    };
    StringBuilder<wchar_t> wide;
    wide.Add(cargoCult.begin(), cargoCult.end()).AppendLine();
        // use ToString(), just like .net
    std::wcout << wide.ToString() << std::endl;
    // javascript-like join.
    std::wcout << wide.Join(L" _n") << std::endl;

    ////////////////////////////////////
    // Performance tests
    ////////////////////////////////////
    TestPerformance(wide, cargoCult);
#ifdef __USE_POSIX199309
    AccurateTestPerformance(wide, cargoCult);
#endif // def __USE_POSIX199309
    return 0;
}

任何情况下,当连接超过几个字符串时,当心std::accumulate函数。

merge

int main(){    vector<int> vector1;    vector<int> vector2;    vector<int> vector3;    for (int i = 0; i < 10; ++i) {        vector1.push_back;    }    for (int i = 1; i < 10; i+=2) {        vector2.push_back;    }    vector3.resize(vector1.size()+vector2.size;    merge(vector1.begin(),vector1.end(),vector2.begin(),vector2.end(),vector3.begin;    for_each(vector3.begin(),vector3.end(),[]{cout<<x<<" ";});    return  0;}//0 1 1 2 3 3 4 5 5 6 7 7 8 9 9

Aync异步

接下来的例子展示了当我们试图缓存一部方法返回值时的一个普遍问题:

例2 枚举类型的装箱

下面的这个例子是导致新的C#
和VB编译器由于频繁的使用枚举类型,特别是在Dictionary中做查找操作时分配了大量内存的原因。

public enum Color { Red, Green, Blue }
public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

问题非常隐蔽,PerfView会告诉你enmu.GetHashCode()由于内部实现的原因产生了装箱操作,该方法会在底层枚举类型的表现形式上进行装箱,如果仔细看PerfView,会看到每次调用GetHashCode会产生两次装箱操作。编译器插入一次,.NET
Framework插入另外一次。

解决方法:

通过在调用GetHashCode的时候将枚举的底层表现形式进行强制类型转换就可以避免这一装箱操作。

((int)color).GetHashCode()

另一个使用枚举类型经常产生装箱的操作时enum.HasFlag。传给HasFlag的参数必须进行装箱,在大多数情况下,反复调用HasFlag通过位运算测试非常简单和不需要分配内存。

要牢记基本要领第一条,不要过早优化。并且不要过早的开始重写所有代码。
需要注意到这些装箱的耗费,只有在通过工具找到并且定位到最主要问题所在再开始修改代码。

预定义函数对象

#include <iostream>#include <algorithm>#include <set>#include <functional>using namespace std;int main() {    plus<int> intAdd;    int x = 10;    int y = 20;    int z = intAdd;    cout<<z;    plus<string> stringAdd;    string s1 = "aaa";    string s2 = "bbb";    string s3;    s3 = stringAdd;    cout<<"n"<<s3;    cout<<endl;    vector<string> vector1;    vector1.push_back("bbb");    vector1.push_back("aaa");    vector1.push_back("ddd");    vector1.push_back("ccc");    //从大到小排序    sort(vector1.begin(),vector1.end(),greater<string>;    for_each(vector1.begin(),vector1.end(),[]{cout<<x<<"   ";});//匿名函数    //计数    vector1.push_back("ccc");    vector1.push_back("ccc");    //函数适配器,equal_to有两个参数,bind2nd将预定义函数和参数绑定    int result = count_if(vector1.begin(),vector1.end(),bind2nd(equal_to<string>(),"ccc"));    cout<<"n"<<result;    return 0;}/*30aaabbbddd   ccc   bbb   aaa3*/

字符串

字符串操作是引起内存分配的最大元凶之一,通常在PerfView中占到前五导致内存分配的原因。应用程序使用字符串来进行序列化,表示JSON和REST。在不支持枚举类型的情况下,字符串可以用来与其他系统进行交互。当我们定位到是由于string操作导致对性能产生严重影响的时候,需要留意string类的Format(),Concat(),Split(),Join(),Substring()等这些方法。使用StringBuilder能够避免在拼接多个字符串时创建多个新字符串的开销,但是StringBuilder的创建也需要进行良好的控制以避免可能会产生的性能瓶颈。

要领四:所有的都与内存分配相关

你可能会想,编写响应及时的基于.NET的应用程序关键在于采用好的算法,比如使用快速排序替代冒泡排序,但是实际情况并不是这样。编写一个响应良好的app的最大因素在于内存分配,特别是当app非常大或者处理大量数据的时候。

在使用新的编译器API开发响应良好的IDE的实践中,大部分工作都花在了如何避免开辟内存以及管理缓存策略。PerfView追踪显示新的C#
和VB编译器的性能基本上和CPU的性能瓶颈没有关系。编译器在读入成百上千甚至上万行代码,读入元数据活着产生编译好的代码,这些操作其实都是I/O
bound
密集型。UI线程的延迟几乎全部都是由于垃圾回收导致的。.NET框架对垃圾回收的性能已经进行过高度优化,他能够在应用程序代码执行的时候并行的执行垃圾回收的大部分操作。但是,单个内存分配操作有可能会触发一次昂贵的垃圾回收操作,这样GC会暂时挂起所有线程来进行垃圾回收(比如
Generation
2型的垃圾回收.aspx))

sort

class student{public:    string name;    int age;    student(){}    student(string _name,int _age){        age=_age;        name = _name;    }};int main(){    vector<student> vector1;    student student1("xiaosun",22);    student student2("laoli",21);    student student3("gaozong",23);    vector1.push_back;    vector1.push_back;    vector1.push_back;    for_each(vector1.begin(),vector1.end(),[](student s){cout<<s.name<<" "<<s.age<<endl;});    sort(vector1.begin(),vector1.end(),[](student a,student b){ return a.age>b.age;});    cout<<"-----------n";    for_each(vector1.begin(),vector1.end(),[](student s){cout<<s.name<<" "<<s.age<<endl;});    return  0;}/*xiaosun 22laoli 21gaozong 23-----------gaozong 23xiaosun 22laoli 21*/

要领二:没有评测,便是猜测

剖析和测量不会撒谎。测评可以显示CPU是否满负荷运转或者是存在磁盘I/O阻塞。测评会告诉你应用程序分配了什么样的以及多大的内存,以及是否CPU花费了很多时间在    
垃圾回收.aspx)上。

应该为关键的用户体验或者场景设置性能目标,并且编写测试来测量性能。通过使用科学的方法来分析性能不达标的原因的步骤如下:使用测评报告来指导,假设可能出现的情况,并且编写实验代码或者修改代码来验证我们的假设或者修正。如果我们设置了基本的性能指标并且经常测试,就能够避免一些改变导致性能的回退(regression),这样就能够避免我们浪费时间在一些不必要的改动中。

要领三:好工具很重要

好的工具能够让我们能够快速的定位到影响性能的最大因素(CPU,内存,磁盘)并且能够帮助我们定位产生这些瓶颈的代码。微软已经发布了很多性能测试工具比如:Visual
Studio Profiler,
Windows Phone Analysis
Tool, 以及
PerfView.

PerfView是一款免费且性能强大的工具,他主要关注影响性能的一些深层次的问题(磁盘
I/O,GC 事件,内存),后面会展示这方面的例子。我们能够抓取性能相关的
Event Tracing for
Windows.aspx)(ETW)事件并能以应用程序,进程,堆栈,线程的尺度查看这些信息。PerfView能够展示应用程序分配了多少,以及分配了何种内存以及应用程序中的函数以及调用堆栈对内存分配的贡献。这些方面的细节,您可以查看随工具下载发布的关于PerfView的非常详细的帮助,Demo以及视频教程(比如Channel9
上的视频教程)

replace

int main(){    vector<int> vector1;    for (int i = 0; i < 10; ++i) {        vector1.push_back;    }    replace(vector1.begin(),vector1.end;    for_each(vector1.begin(),vector1.end(),[]{cout<<s<<" ";});    return  0;}

缓存

性能优化的一个常用技巧是缓存结果。但是如果缓存没有大小上限或者良好的资源释放机制就会导致内存泄漏。在处理大数据量的时候,如果在缓存中缓存了过多数据就会占用大量内存,这样导致的垃圾回收开销就会超过在缓存中查找结果所带来的好处。

例1 string方法和其值类型参数

下面的示例代码演示了潜在的不必要的装箱以及在大的系统中的频繁的装箱操作。

public class Logger
{
    public static void WriteLine(string s)
    {
        /*...*/
    }
}
public class BoxingExample
{
    public void Log(int id, int size)
    {
        var s = string.Format("{0}:{1}", id, size);
        Logger.WriteLine(s);
    }
}

这是一个日志基础类,因此app会很频繁的调用Log函数来记日志,可能该方法会被调用millons次。问题在于,调用string.Format方法会调用其重载.aspx)的接受一个string类型和两个Object类型的方法:

String.Format Method (String, Object, Object)

该重载方法要求.NET Framework
int型装箱为object类型然后将它传到方法调用中去。为了解决这一问题,方法就是调用id.ToString()size.ToString()方法,然后传入到string.Format
方法中去,调用ToString()方法的确会导致一个string的分配,但是在string.Format方法内部不论怎样都会产生string类型的分配。

你可能会认为这个基本的调用string.Format
仅仅是字符串的拼接,所以你可能会写出这样的代码:

var s = id.ToString() + ':' + size.ToString();

实际上,上面这行代码也会导致装箱,因为上面的语句在编译的时候会调用:

string.Concat(Object, Object, Object);

这个方法,.NET Framework 必须对字符常量进行装箱来调用Concat方法。

解决方法:

完全修复这个问题很简单,将上面的单引号替换为双引号即将字符常量换为字符串常量就可以避免装箱,因为string类型的已经是引用类型了。

var s = id.ToString() + ":" + size.ToString();

6、deque

要点:双端数组

  1. std::distance()求坐标
  2. std::find()查找元素的迭代器,找不到返回 iter end()

void print(deque<int> dl) {    for (deque<int>::iterator iter = dl.begin(); iter != dl.end(); iter++) {        cout << *iter << "t";    }    cout << "nfront:" << dl.front() << "   end:" << dl.back() << endl;}int main() {    deque<int> dl;    dl.push_back;    dl.push_back;    dl.push_back;    dl.push_front;    dl.push_front;    dl.push_front;    print;    dl.pop_back();    dl.pop_front();    print;    auto x = find(dl.begin(),dl.end;    if(x!=dl.end        cout<<distance(dl.begin<<endl;//求坐标索引    else        cout<<"Not Found"<<endl;    return 0;}

例1 string方法和其值类型参数

下面的示例代码演示了潜在的不必要的装箱以及在大的系统中的频繁的装箱操作。

 

[js] view
plaincopyprint?

  1. public class Logger  
  2. {  
  3.     public static void WriteLine(string s)  
  4.     {  
  5.         /*…*/  
  6.     }  
  7. }  
  8. public class BoxingExample  
  9. {  
  10.     public void Log(int id, int size)  
  11.     {  
  12.         var s = string.Format(“{0}:{1}”, id, size);  
  13.         Logger.WriteLine(s);  
  14.     }  
  15. }  

    public class Logger
    {

    public static void WriteLine(string s)
    {
        /*...*/
    }
    

    }
    public class BoxingExample
    {

    public void Log(int id, int size)
    {
        var s = string.Format("{0}:{1}", id, size);
        Logger.WriteLine(s);
    }
    

    }

 

这是一个日志基础类,因此app会很频繁的调用Log函数来记日志,可能该方法会被调用millons次。问题在于,调用string.Format方法会调用其    
重载.aspx)的接受一个string类型和两个Object类型的方法:        

 

[js] view
plaincopyprint?

  1. String.Format Method (String, Object, Object)  

    String.Format Method (String, Object, Object)

 

该重载方法要求.NET Framework
int型装箱为object类型然后将它传到方法调用中去。为了解决这一问题,方法就是调用id.ToString()size.ToString()方法,然后传入到string.Format
方法中去,调用ToString()方法的确会导致一个string的分配,但是在string.Format方法内部不论怎样都会产生string类型的分配。

你可能会认为这个基本的调用string.Format
仅仅是字符串的拼接,所以你可能会写出这样的代码:

 

[js] view
plaincopyprint?

  1. var s = id.ToString() + ‘:’ + size.ToString();  

    var s = id.ToString() + ‘:’ + size.ToString();

 

 

实际上,上面这行代码也会导致装箱,因为上面的语句在编译的时候会调用:

 

[js] view
plaincopyprint?

  1. string.Concat(Object, Object, Object);  

    string.Concat(Object, Object, Object);

 

这个方法,.NET Framework 必须对字符常量进行装箱来调用Concat方法。

解决方法:

完全修复这个问题很简单,将上面的单引号替换为双引号即将字符常量换为字符串常量就可以避免装箱,因为string类型的已经是引用类型了。

 

[js] view
plaincopyprint?

  1. var s = id.ToString() + “:” + size.ToString();  

    var s = id.ToString() + “:” + size.ToString();

 

类和结构

不甚严格的讲,在优化应用程序方面,类和结构提供了一种经典的空间/时间的权衡(trade
off)。在x86机器上,每个类即使没有任何字段,也会分配12 byte的空间
(译注:来保存类型对象指针和同步索引块),但是将类作为方法之间参数传递的时候却十分高效廉价,因为只需要传递指向类型实例的指针即可。结构体如果不撞向的话,不会再托管堆上产生任何内存分配,但是当将一个比较大的结构体作为方法参数或者返回值得时候,需要CPU时间来自动复制和拷贝结构体,然后将结构体的属性缓存到本地便两种以避免过多的数据拷贝。

3、hash_map

hash函数->直接地址,比较函数->解决冲突

这两个参数刚好是我们在使用hash_map时需要指定的参数.

hash_map<int, string> mymap;

等同于:hash_map<int, string, hash, equal_to > mymap;

#include <hash_map>#include <algorithm>#include <iostream>using namespace std;int main() {    __gnu_cxx::hash_map<int,string> mymap;    mymap[1]="风琴杨";    mymap[2]="林青霞";    if (mymap.find!=mymap.end        cout<<mymap[1]<<endl;    cout<<mymap[10]<<"  null"<<endl;    return 0;}//output/*风琴杨  null */

例5 Lambdas表达式,List<T>,以及IEnumerable<T>

下面的例子使用    
LINQ以及函数式风格的代码来通过编译器模型给定的名称来查找符号。

 

[js] view
plaincopyprint?

  1. class Symbol   
  2. {   
  3.     public string Name { get; private set; } /*…*/  
  4. }  
  5. class Compiler   
  6. {   
  7.     private List<Symbol> symbols;   
  8.     public Symbol FindMatchingSymbol(string name)   
  9.     {   
  10.         return symbols.FirstOrDefault(s => s.Name == name);   
  11.     }  
  12. }  

    class Symbol
    {

    public string Name { get; private set; } /*...*/
    

    }
    class Compiler
    {

    private List<Symbol> symbols; 
    public Symbol FindMatchingSymbol(string name) 
    { 
        return symbols.FirstOrDefault(s => s.Name == name); 
    }
    

    }

 

新的编译器和IDE
体验基于调用FindMatchingSymbol,这个调用非常频繁,在此过程中,这么简单的一行代码隐藏了基础内存分配开销。为了展示这其中的分配,我们首先将该单行函数拆分为两行:

 

[js] view
plaincopyprint?

  1. Func<Symbol, bool> predicate = s => s.Name == name;   
  2. return symbols.FirstOrDefault(predicate);  

    Func predicate = s => s.Name == name;
    return symbols.FirstOrDefault(predicate);

 

第一行中,    
lambda表达式“s=>s.Name==name
是对本地变量name的一个    
闭包。这就意味着需要分配额外的对象来为        
委托对象predict分配空间,需要一个分配一个静态类来保存环境从而保存name的值。编译器会产生如下代码:

 

[js] view
plaincopyprint?

  1. // Compiler-generated class to hold environment state for lambda   
  2. private class Lambda1Environment   
  3. {   
  4.     public string capturedName;   
  5.     public bool Evaluate(Symbol s)   
  6.     {   
  7.         return s.Name == this.capturedName;  
  8.     }   
  9. }  
  10.   
  11. // Expanded Func<Symbol, bool> predicate = s => s.Name == name;   
  12. Lambda1Environment l = new Lambda1Environment()   
  13. {   
  14.     capturedName = name  
  15. };   
  16. var predicate = new Func<Symbol, bool>(l.Evaluate);  

    // Compiler-generated class to hold environment state for lambda
    private class Lambda1Environment
    {

    public string capturedName; 
    public bool Evaluate(Symbol s) 
    { 
        return s.Name == this.capturedName;
    } 
    

    }

    // Expanded Func predicate = s => s.Name == name;
    Lambda1Environment l = new Lambda1Environment()
    {

    capturedName = name
    

    };
    var predicate = new Func(l.Evaluate);

 

两个new操作符(第一个创建一个环境类,第二个用来创建委托)很明显的表明了内存分配的情况。

现在来看看FirstOrDefault方法的调用,他是IEnumerable<T>类的扩展方法,这也会产生一次内存分配。因为FirstOrDefault使用IEnumerable<T>作为第一个参数,可以将上面的展开为下面的代码:

 

[js] view
plaincopyprint?

  1. // Expanded return symbols.FirstOrDefault(predicate) …   
  2. IEnumerable<Symbol> enumerable = symbols;  
  3. IEnumerator<Symbol> enumerator = enumerable.GetEnumerator();   
  4. while (enumerator.MoveNext())  
  5. {   
  6.     if (predicate(enumerator.Current))   
  7.         return enumerator.Current;   
  8. }   
  9. return default(Symbol);  

    // Expanded return symbols.FirstOrDefault(predicate) …
    IEnumerable enumerable = symbols;
    IEnumerator enumerator = enumerable.GetEnumerator();
    while (enumerator.MoveNext())
    {

    if (predicate(enumerator.Current)) 
        return enumerator.Current; 
    

    }
    return default(Symbol);

 

symbols变量是类型为List<T>的变量。List<T>集合类型实现了IEnumerable<T>即可并且清晰地定义了一个    
迭代器.aspx),List<T>的迭代器使用了一种结构体来实现。使用结构而不是类意味着通常可以避免任何在托管堆上的分配,从而可以影响垃圾回收的效率。枚举典型的用处在于方便语言层面上使用foreach循环,他使用enumerator结构体在调用推栈上返回。递增调用堆栈指针来为对象分配空间,不会影响GC对托管对象的操作。

在上面的展开FirstOrDefault调用的例子中,代码会调用IEnumerabole<T>接口中的GetEnumerator()方法。将symbols赋值给IEnumerable<Symbol>类型的enumerable
变量,会使得对象丢失了其实际的List<T>类型信息。这就意味着当代码通过enumerable.GetEnumerator()方法获取迭代器时,.NET   
Framework
必须对返回的值(即迭代器,使用结构体实现)类型进行装箱从而将其赋给IEnumerable<Symbol>类型的(引用类型)
enumerator变量。

解决方法:

解决办法是重写FindMatchingSymbol方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。

 

[js] view
plaincopyprint?

  1. public Symbol FindMatchingSymbol(string name)   
  2. {   
  3.     foreach (Symbol s in symbols)  
  4.     {   
  5.         if (s.Name == name)   
  6.             return s;   
  7.     }   
  8.     return null;   
  9. }  

    public Symbol FindMatchingSymbol(string name)
    {

    foreach (Symbol s in symbols)
    { 
        if (s.Name == name) 
            return s; 
    } 
    return null; 
    

    }

 

代码中并没有使用LINQ扩展方法,lambdas表达式和迭代器,并且没有额外的内存分配开销。这是因为编译器看到symbol
List<T>类型的集合,因为能够直接将返回的结构性的枚举器绑定到类型正确的本地变量上,从而避免了对struct类型的装箱操作。原先的代码展示了C#语言丰富的表现形式以及.NET   
Framework
强大的生产力。该着后的代码则更加高效简单,并没有添加复杂的代码而增加可维护性。

例4 StringBuilder

本例中使用StringBuilder。下面的函数用来产生泛型类型的全名:

public class Example 
{ 
    // Constructs a name like "SomeType<T1, T2, T3>" 
    public string GenerateFullTypeName(string name, int arity) 
    { 
        StringBuilder sb = new StringBuilder();
        sb.Append(name);
        if (arity != 0)
        { 
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            } 
            sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
        }
        return sb.ToString(); 
    }
}

注意力集中到StringBuilder实例的创建上来。代码中调用sb.ToString()会导致一次内存分配。在StringBuilder中的内部实现也会导致内部内存分配,但是我们如果想要获取到string类型的结果化,这些分配无法避免。

解决方法:

要解决StringBuilder对象的分配就使用缓存。即使缓存一个可能被随时丢弃的单个实例对象也能够显著的提高程序性能。下面是该函数的新的实现。除了下面两行代码,其他代码均相同

// Constructs a name like "Foo<T1, T2, T3>" 
public string GenerateFullTypeName(string name, int arity)
{
    StringBuilder sb = AcquireBuilder(); /* Use sb as before */ 
    return GetStringAndReleaseBuilder(sb);
}

关键部分在于新的
AcquireBuilder()GetStringAndReleaseBuilder()方法:

[ThreadStatic]
private static StringBuilder cachedStringBuilder;

private static StringBuilder AcquireBuilder()
{
    StringBuilder result = cachedStringBuilder;
    if (result == null)
    {
        return new StringBuilder();
    } 
    result.Clear(); 
    cachedStringBuilder = null; 
    return result;
}

private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
    string result = sb.ToString(); 
    cachedStringBuilder = sb; 
    return result;
}

上面方法实现中使用了thread-static.aspx)字段来缓存StringBuilder对象,这是由于新的编译器使用了多线程的原因。很可能会忘掉这个ThreadStatic声明。Thread-static字符为每个执行这部分的代码的线程保留一个唯一的实例。

如果已经有了一个实例,那么AcquireBuilder()方法直接返回该缓存的实例,在清空后,将该字段或者缓存设置为null。否则AcquireBuilder()创建一个新的实例并返回,然后将字段和cache设置为null

当我们对StringBuilder处理完成之后,调用GetStringAndReleaseBuilder()方法即可获取string结果。然后将StringBuilder保存到字段中或者缓存起来,然后返回结果。这段代码很可能重复执行,从而创建多个StringBuilder对象,虽然很少会发生。代码中仅保存最后被释放的那个StringBuilder对象来留作后用。新的编译器中,这种简单的的缓存策略极大地减少了不必要的内存分配。.NET
Framework
和MSBuild
中的部分模块也使用了类似的技术来提升性能。

简单的缓存策略必须遵循良好的缓存设计,因为他有大小的限制cap。使用缓存可能比之前有更多的代码,也需要更多的维护工作。我们只有在发现这是个问题之后才应该采缓存策略。PerfView已经显示出StringBuilder对内存的分配贡献相当大。

count_if

int main(){    vector<int> vector1;    vector1.push_back;    vector1.push_back;    vector1.push_back;    vector1.push_back;    vector1.push_back;    int nums= count_if(vector1.begin(),vector1.end(),[]{ return x>3;});    if {        cout<<"找到了";    } else        cout<<"大于3有"<<nums<<"个";    return  0;}//大于3有2个

字典

在很多应用程序中,Dictionary用的很广,虽然字非常方便和高校,但是经常会使用不当。在Visual   
Studio以及新的编译器中,使用性能分析工具发现,许多dictionay只包含有一个元素或者干脆是空的。一个空的Dictionay结构内部会有10个字段在x86机器上的托管堆上会占据48个字节。当需要在做映射或者关联数据结构需要事先常量时间查找的时候,字典非常有用。但是当只有几个元素,使用字典就会浪费大量内存空间。相反,我们可以使用List<KeyValuePair<K,V>>结构来实现便利,对于少量元素来说,同样高校。如果仅仅使用字典来加载数据,然后读取数据,那么使用一个具有N(log(N))的查找效率的有序数组,在速度上也会很快,当然这些都取决于的元素的个数。

其他一些影响性能的杂项

在大的app或者处理大量数据的app中,还有几点可能会引发潜在的性能问题。

replace_if

int main(){    vector<int> vector1;    for (int i = 0; i < 10; ++i) {        vector1.push_back;    }    replace_if(vector1.begin(),vector1.end(),[]{ return x>5;},11);    for_each(vector1.begin(),vector1.end(),[]{cout<<s<<" ";});    return  0;}//0 1 2 3 4 5 11 11 11 11

装箱

装箱发生在当通常分配在线程栈上或者数据结构中的值类型,或者临时的值需要被包装到对象中的时候(比如分配一个对象来存放数据,活着返回一个指针给一个Object对象)。.NET框架由于方法的签名或者类型的分配位置,有些时候会自动对值类型进行装箱。将值类型包装为引用类型会产生内存分配。.NET框架及语言会尽量避免不必要的装箱,但是有时候在我们没有注意到的时候会产生装箱操作。过多的装箱操作会在应用程序中分配成M上G的内存,这就意味着垃圾回收的更加频繁,也会花更长时间。

在PerfView中查看装箱操作,只需要开启一个追踪(trace),然后查看应用程序名字下面的GC
Heap Alloc
项(记住,PerfView会报告所有的进程的资源分配情况),如果在分配相中看到了一些诸如System.Int32和System.Char的值类型,那么就发生了装箱。选择一个类型,就会显示调用栈以及发生装箱的操作的函数。

例6 缓存异步方法

Visual Studio IDE
的特性在很大程度上建立在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍能够保持Visual
Stuido能够响应。下面是获取语法树的第一个版本的代码:

class Parser 
{
    /*...*/ 
    public SyntaxTree Syntax
    { 
        get; 
    } 

    public Task ParseSourceCode() 
    {
        /*...*/ 
    } 
}
class Compilation 
{ 
    /*...*/ 
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax;
    } 
}

可以看到调用GetSyntaxTreeAsync()
方法会实例化一个Parser对象,解析代码,然后返回一个Task<SyntaxTree>对象。最耗性能的地方在为Parser实例分配内存并解析代码。方法中返回一个Task对象,因此调用者可以await解析工作,然后释放UI线程使得可以响应用户的输入。

由于Visual Studio的一些特性可能需要多次获取相同的语法树,
所以通常可能会缓存解析结果来节省时间和内存分配,但是下面的代码可能会导致内存分配:

class Compilation 
{ /*...*/
    private SyntaxTree cachedResult;
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        if (this.cachedResult == null) 
        { 
            var parser = new Parser(); // allocation 
            await parser.ParseSourceCode(); // expensive 
            this.cachedResult = parser.Syntax; 
        } 
        return this.cachedResult;
    }
}

代码中有一个SynataxTree类型的名为cachedResult的字段。当该字段为空的时候,GetSyntaxTreeAsync()执行,然后将结果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree对象。问题在于,当有一个类型为Task<SyntaxTree>
类型的async异步方法时,想要返回SyntaxTree的值,编译器会生出代码来分配一个Task来保存执行结果(通过使用Task<SyntaxTree>.FromResult())。Task会标记为完成,然后结果立马返回。分配Task对象来存储执行的结果这个动作调用非常频繁,因此修复该分配问题能够极大提高应用程序响应性。

解决方法:

要移除保存完成了执行任务的分配,可以缓存Task对象来保存完成的结果。

class Compilation 
{ /*...*/
    private Task<SyntaxTree> cachedResult;
    public Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync()); 
    }
    private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync() 
    {
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax; 
    } 
}

代码将cachedResult 类型改为了Task<SyntaxTree>
并且引入了async帮助函数来保存原始代码中的GetSyntaxTreeAsync()函数。GetSyntaxTreeAsync函数现在使用
null操作符,来表示当cachedResult不为空时直接返回,为空时GetSyntaxTreeAsync调用GetSyntaxTreeUncachedAsync()然后缓存结果。注意GetSyntaxTreeAsync并没有await调用GetSyntaxTreeUncachedAsync。没有使用await意味着当GetSyntaxTreeUncachedAsync返回Task类型时,GetSyntaxTreeAsync
也立即返回Task
现在缓存的是Task,因此在返回缓存结果的时候没有额外的内存分配。

发表评论

电子邮件地址不会被公开。 必填项已用*标注