성능은 소프트웨어 제품의 품질을 고려하는 중요한 지표이자 제품의 기능만큼이나 중요합니다. 사용자가 소프트웨어 제품을 선택할 때 기본적으로 유사한 제품의 성능을 직접 테스트하고 비교하게 됩니다. 구매할 소프트웨어를 선택할 때 중요한 요소 중 하나입니다.
소프트웨어 성능이란 무엇을 의미합니까?
1. 메모리 소비 다운그레이드
소프트웨어 개발에서 메모리 소비는 일반적으로 부차적인 고려 사항입니다. 왜냐하면 오늘날의 컴퓨터는 일반적으로 많은 경우에 상대적으로 큰 메모리를 사용하기 때문입니다. , 성능 최적화의 수단은 시간과 공간을 교환하는 것입니다. 그러나 이것이 메모리를 무분별하게 낭비할 수 있다는 의미는 아닙니다. 대용량 데이터가 포함된 사용 사례를 지원해야 하는 경우, 메모리가 소진되면 운영 체제는 내부 메모리와 외부 메모리를 자주 교환합니다. 실행 속도의 급격한 저하
2. 실행 속도 향상
로딩 속도.
특정 작업에 대한 응답 속도. 클릭, 키보드 입력, 스크롤, 정렬 및 필터링 등이 포함됩니다.
성능 최적화 원칙
요구 사항 이해
MultiRow 제품의 성능 요구 사항 중 하나는 "수백만"입니다. 데이터 바인딩 행의 부드러운 스크롤 "전체 MultiRow 프로젝트는 이 목표를 염두에 두고 개발되었습니다.
병목 현상 이해
경험에 따르면 성능 소비의 99%는 코드의 1%로 인해 발생합니다. 따라서 대부분의 성능 최적화는 이 1% 병목 코드를 목표로 합니다. 구체적인 구현은 두 단계로 나누어집니다. 첫째, 병목 현상을 식별하고, 둘째, 병목 현상을 제거합니다.
과장하지 마세요
우선 성능 최적화 자체에는 비용이 든다는 점을 깨달아야 합니다. 이 비용은 성능 최적화에 소요되는 작업량에만 반영되는 것이 아닙니다. 또한 성능 최적화, 추가 유지 관리 비용, 새로운 버그 도입, 추가 메모리 오버헤드 등을 위해 작성된 복잡한 코드도 포함됩니다. 일반적인 문제는 소프트웨어 개발을 처음 접하는 일부 학생들이 불필요한 지점에 성능 최적화 기술이나 디자인 패턴을 기계적으로 적용하여 불필요한 복잡성을 가져오는 것입니다. 성능 최적화에는 종종 이점과 비용 간의 균형이 필요합니다.
성능 병목 현상을 찾는 방법
이전 섹션에서 언급했듯이 성능 최적화의 첫 번째 단계는 성능 병목 현상을 찾는 것입니다. 일부 관행에서는 성능 병목 현상이 발생합니다.
1. 메모리 소모량을 구하는 방법
다음 코드는 특정 작업의 메모리 소모량을 구할 수 있습니다.
// 在这里写一些可能消耗内存的代码,例如,如果想了解创建一个GcMultiRow软件需要多少内存可以执行以下代码 long start = GC.GetTotalMemory(true); var gcMulitRow1 = new GcMultiRow(); GC.Collect(); // 确保所有内存都被GC回收 GC.WaitForFullGCComplete(); long end = GC.GetTotalMemory(true); long useMemory = end - start;
2. 소요 시간을 구하는 방법
다음 코드는 특정 작업의 소요 시간을 구하는 코드입니다.
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start(); for (int i = 0; i < 1000; i++) { gcMultiRow1.Sort(); } watch.Stop(); var useTime = (double)watch.ElapsedMilliseconds / 1000;
여기서 연산 루프를 1000번 실행하고 마지막으로 소요된 시간을 1000으로 나누어 최종 소요 시간을 결정합니다. 결과는 더욱 정확하고 안정적일 수 있으며 예상치 못한 데이터는 제거될 수 있습니다.
3. CodeReview를 통해 성능 문제를 발견합니다.
많은 경우 CodeReview를 통해 성능 문제를 발견할 수 있습니다. 많은 양의 데이터가 포함된 루프에는 특별한 주의를 기울여야 합니다. 루프 내의 논리는 가능한 한 빨리 실행되어야 합니다. 4.ANTS 성능 프로파일러
ANTS 프로파일러는 강력한 성능 테스트 소프트웨어입니다. 이는 성능 병목 현상을 매우 잘 찾는 데 도움이 될 수 있습니다. 이 소프트웨어를 사용하여 성능 병목 현상을 찾으면 절반의 노력으로 두 배의 결과를 얻을 수 있습니다. 이 도구를 능숙하게 사용하면 성능 문제가 있는 코드를 빠르고 정확하게 찾을 수 있습니다. 이 도구는 강력하지만 완벽하지는 않습니다. 우선, 이것은 유료 소프트웨어이며 부서에는 라이센스 번호가 몇 개 밖에 없습니다. 둘째, 이 소프트웨어의 작동 원리는 시간을 기록하기 위해 IL에 몇 가지 후크를 추가하는 것입니다. 따라서 분석 중에는 소프트웨어의 실행 속도가 실제 작업보다 느려지므로 얻은 데이터가 100% 정확하지는 않습니다. 소프트웨어에서 분석한 데이터는 문제를 빠르게 찾는 데 도움이 되는 참고 자료로 사용해야 합니다. 전적으로 의존하지 말고 다른 기술과 결합하여 프로그램 성능을 분석하십시오.
성능 최적화를 위한 방법 및 기법
성능 문제를 찾은 후에는 여러 가지 해결 방법이 있습니다. 이 장에서는 몇 가지 성능 최적화 기술과 사례를 소개합니다.
1. 프로그램 구조 최적화
프로그램 구조는 설계 시 고려하고 성능 요구 사항을 충족할 수 있는지 평가해야 합니다. 나중에 성능 문제가 발견되면 구조 조정을 고려해야 하는데, 이로 인해 오버헤드가 많이 발생하게 됩니다. 예:
1.1 GcMultiRowGcMultiRow는 1백만 행의 데이터를 지원해야 합니다. 각 행에 10개의 열이 있고 1천만 개의 셀이 필요하며 각 셀에 많은 속성이 있다고 가정합니다. 최적화가 수행되지 않으면 데이터 양이 많을 때 GcMultiRow 소프트웨어의 메모리 오버헤드가 상당히 커집니다. GcMultiRow가 채택한 솔루션은 해시 테이블을 사용하여 행 데이터를 저장하는 것입니다. 사용자가 변경한 행만 해시 테이블에 배치되고, 변경되지 않은 대부분의 행은 템플릿으로 직접 대체됩니다. 이는 메모리 절약 목적을 달성합니다.
1.2 Spread for WPF/Silverlight (SSL)WPF的画法和Winform不同,是通过组合View元素的方法实现的。SSL同样支持百万级的数据量,但是又不能给每个单元格都分配一个View。所以SSL使用了VirtualizePanel来实现画法。思路是每一个View是一个Cell的展示模块。可以和Cell的数据模块分离。这样。只需要为显示出来的Cell创建View。当发生滚动时会有一部分Cell滚出屏幕,有一部分Cell滚入屏幕。这时,让滚出屏幕的Cell和View分离。然后再复用这部分View给新进入屏幕的Cell。如此循环。这样只需要几百个View就可以支持很多的Cell。
2. 缓存
缓存(Cache)是性能优化中最常用的优化手段.适用的情况是频繁的获取一些数据,而每次获取这些数据需要的时间比较长。这时,第一次获取的时候会用正常的方法,并且在获取之后把数据缓存下来。之后就使用缓存的数据。 如果使用了缓存的优化方法,需要特别注意缓存数据的同步,就是说,如果真实的数据发生了变化,应该及时的清除缓存数据,确保不会因为缓存而使用了错误的数据。 举例:
2.1 使用缓存的情况比较多。最简单的情况就是缓存到一个Field或临时变量里。
for(int i = 0; i < gcMultiRow.RowCount; i++) { // Do something; }
以上代码一般情况下是没有问题的,但是,如果GcMultiRow的行数比较大。而RowCount属性的取值又比较慢的时候就需要使用缓存来做性能优化。
int rowCount = gcMultiRow.RowCount; for (int i = 0; i < rowCount; i++) { // Do something; }
2.2 使用对象池也是一个常见的缓存方案,比使用Field或临时变量稍微复杂一点。 例如,在MultiRow中,画边线,画背景,需要用到大量的Brush和Pen。这些GDI对象每次用之前要创建,用完后要销毁。创建和销毁的过程是比较慢的。GcMultiRow使用的方案是创建一个GDIPool。本质上是一些Dictionary,使用颜色做Key。所以只有第一次取的时候需要创建,以后就直接使用以前创建好的。以下是GDIPool的代码:
public static class GDIPool { Dictionary<Color, Brush > _cacheBrush = new Dictionary<Color, Brush>(); Dictionary<Color, Pen> _cachePen = new Dictionary<Color, Pen>(); public static Pen GetPen(Color color) { Pen pen; if_cachePen.TryGetValue(color, out pen)) { return pen; } pen = new Pen(color); _cachePen.Add(color, pen); return pen; } }
2.3 懒构造
有时候,有的对象创建需要花费较长时间。而这个对象可能并不是所有的场景下都需要使用。这时,使用赖构造的方法可以有效提高性能。 举例:对象A需要内部创建对象B。对象B的构造时间比较长。 一般做法:
public class A { public B _b = new B(); }
一般做法下由于构造对象A的同时要构造对象B导致了A的构造速度也变慢了。优化做法:
public class A { private B _b; public B BProperty { get { if(_b == null) { _b = new B(); } return _b; } } }
优化后,构造A的时候就不需要创建B对象,只有需要使用的时候才需要构造B对象。
2.4 优化算法 优化算法可以有效的提高特定操作的性能,使用一种算法时应该了解算法的适用情况,最好情况和最坏情况。 以GcMultiRow为例,最初MultiRow的排序算法使用了经典的快速排序算法。这看起来是没有问题的,但是,对于表格软件,用户经常的操作是对有序表进行排序,如顺序和倒序之间切换。而经典的快速排序算法的最差情况就是基本有序的情况。所以经典快速排序算法不适合MultiRow。最后通过改的排序算法解决了这个问题。改进的快速排序算法使用了3个中点来代替经典快排的一个中点的算法。每次交换都是从3个中点中选择一个。这样,乱序和基本有序的情况都不是这个算法的最坏情况,从而优化了性能。
2.5 了解Framework提供的数据结构 我们现在工作的.net framework平台,有很多现成的数据数据结构。我们应该了解这些数据结构,提升我们程序的性能:
举例:
2.5.1 string 的加运算符 VS StringBuilder: 字符串的操作是我们经常遇到的基本操作之一。 我们经常会写这样的代码 string str = str1 + str2。当操作的字符串很少的时候,这样的操作没有问题。但是如果大量操作的时候(例如文本文件的Save/Load, Asp.net的Render),这样做就会带来严重的性能问题。这时,我们就应该用StringBuilder来代替string的加操作。
2.5.2 Dictionary VS List Dictionary和List是最常用的两种集合类。选择正确的集合类可以很大的提升程序的性能。为了做出正确的选择,我们应该对Dictionary和List的各种操作的性能比较了解。2.5.3TryGetValue 对于Dictionary的取值,比较直接的方法是如下代码:
if(_dic.ContainKey("Key") { return _dic\["Key"\]; }
当需要大量取值的时候,这样的取法会带来性能问题。优化方法如下:
object value; if(_dic.TryGetValue("Key", out value)) { return value; }
使用TryGetValue可以比先Contain再取值提高一倍的性能。
2.5.4 为Dictionary选择合适的Key。 Dictionary的取值性能很大情况下取决于做Key的对象的Equals和GetHashCode两个方法的性能。如果可以的话使用Int做Key性能最好。如果是一个自定义的Class做Key的话,最好保证以下两点:1. 不同对象的GetHashCode重复率低。2. GetHashCode和Equals方法立即简单,效率高。
2.5.5 List的Sort和BinarySearch性能很好,如果能满足功能需求的话推荐直接使用,而不是自己重写。
List<int> list = new List<int>{3, 10, 15}; list.BinarySearch(10); // 对于存在的值,结果是1 list.BinarySearch(8); // 对于不存在的值,会使用负数表示位置,如查找8时,结果是-2, 查找0结果是-1,查找100结果是-4.
复制代码
2.6 通过异步提升响应时间
2.6.1 多线程
有些操作确实需要花费比较长的时间,如果用户的操作在这段时间卡死会带来很差的用户体验。有时候,使用多线程技术可以解决这个问题 举例: CalculatorEngine在构造的时候要初始化所有的Function。由于Function比较多,初始化时间会比较长。这是就用到了多线程技术,在工作线程中做Function的初始化工作,就不影响主线程快速响应用户的其他操作了。代码如下:
public CalcParser() { if (_functions == null) { lock (_obtainFunctionLocker) { if (_functions == null) { System.Threading.ThreadPool.QueueUserWorkItem((s) => { if (_functions == null) { lock (_obtainFunctionLocker) { if (_functions == null) { _functions = EnsureFunctions(); } } } }); } } } }
这里比较慢的操作就是EnsureFunctions函数,是在另一个线程里执行的,不会影响主线程的响应。当然,使用多线程是一个比较有难度的方案,需要充分考虑跨线程访问和死锁的问题。
2.6.2 加延迟时间
在GcMultiRow实现AutoFilter功能的时候使用了一个类似于延迟执行的方案来提升响应速度。AutoFilter的功能是用户在输入的过程中根据用户的输入更新筛选的结果。数据量大的时候一次筛选需要较长时间,会影响用户的连续输入。使用多线可能是个好的方案,但是使用多线程会增加程序的复杂度。MultiRow的解决方案是当接收到用户的键盘输入消息的时候,并不立即出发Filter,而是等待0.3秒。如果用户在连续输入,会在这0.3秒内再次收到键盘消息,就再等0.3秒。直到连续0.3秒内没有新的键盘消息时再触发Filter。保证了快速响应用户输入的目的。
2.6.3 Application.Idle事件
在GcMultiRow的Designer里,经常要根据当前的状态刷新ToolBar上按钮的Disable/Enable状态。一次刷新需要较长的时间。如果用户连续输入会有卡顿的感觉,影响用户体验。GcMultiRow的优化方案是挂系统的Application.Idle事件。当系统空闲的时候,系统会触发这个事件。接到这个事件表示此时用户已经完成了连续的输入,这时就可以从容的刷新按钮的状态了。
2.6.4 Invalidate, BeginInvoke. PostEvent 平台本身也提供了一些异步方案。
例如;在Winform下,触发一块区域重画的时候,一般不适用Refresh而是Invalidate,这样会触发异步的刷新。在触发之前可以多次Invalidate。BeginInvoke,PostMessage也都可以触发异步的行为。
2.7 了解平台特性
如WPF的DP DP相对于CLR property来说是很慢的,包括Get和Set都很慢,这和一般质感上Get比较快Set比较慢不一样。如果一个DP需要被多次读取的话建议是CLR property做Cache。
2.8 进度条,提升用户体验
有时候,以上提到的方案都没有办法快速响应用户操作,进度条,一直转圈圈的图片,提示性文字如"你的操作可能需要较长时间请耐心等待"。都可以提升用户体验。可以作为最后方案来考虑。
目前已有很多使用C#编写的开发工具,其中值得一提的是ComponentOne Studio Enterprise,这是一款专注于企业应用的.NET全功能控件套包,支持WinForms、WPF、UWP、ASP.NET MVC等多个平台,帮助在缩减成本的同时,提前交付丰富的桌面、Web和移动企业应用。