ASP.NET夜話之二十一asp.net網站的性能優化

本篇主要講述在ASP.NET中如何提高程序性能。提高程序性能的方法主要從編碼和數據操作及優化配置三方面,本章要講述的知識點有:

l  程序編碼優化

l  數據操作優化

l  配置優化

l  總結

21.1 程序編碼優化

從編碼方面提高程序性能的方法主要涉及到集合操作、字符串連接、類型轉換等。

21.1.1 集合操作

.NET Framework中提供了很多集合類,如ArrayListBitArrayHashtableQueueSortedListStackListDictionaryNameValueCollectionOrderedDictionaryStringCollectionList<T>及數組等,要了解各個集合的特性,選擇合適的集合。在所有的集合中數組是性能最高的,如果要存儲的數據類型一致和容量固定,特別是對值類型的數組進行操作時沒有裝箱和拆箱操作,效率極高。

在選擇集合類型時應考慮幾點:

1)集合中的元素類型是否是一致的,比如集合中將要存儲的元素都是int或者都是string類型的就可以考慮使用數組或者泛型集合,這樣在存儲數值類型元素就可以避免裝箱拆箱操作,即使是引用類型的元素也可以避免類型轉換操作。

2)集合中的元素個數是否是固定的,如果集合中存儲的元素是固定的並且元素類型是一致的就可以使用數組來存儲。

3)將來對集合的操作集中在那些方面,如果對集合的操作以查找居多可以考慮HashTable或者Dictionary<TKey,TValue>這樣的集合,因爲在.NET Framework中對這類集合採用了特殊機制,所以在查找時比較的次數比其它集合要少。

另外,在使用可變集合時如果不制定初始容量大小,系統會使用一個默認值來指定可變集合的初始容量大小,如果將來元素個數超過初始容量大小就會先在內部重新構建一個集合,再將原來集合中的元素複製到新集合中,可以在實例化可變集合時指定一個相對較大的初始容量,這樣在向可變集合中添加大量元素時就可以避免集合擴充容量帶來的性能損失。

下面以一個例子演示一下數組、ArrayListList<T>集合操作的例子。頁面的設計代碼如下:

對上面的程序代碼做幾點說明:

1)上面的代碼僅僅是給集合中的元素賦值,然後將集合中的元素取出來,分別用了數組、ArrayListList<T>泛型集合,並且操作了不同的次數。

2)在開始運行時獲取到系統的當前時間,然後在運行結束之後再次獲取系統時間,兩次時間之差就是程序運行這段代碼所花費的時間,這是一個TimeSpan類型的變量。

3)爲了將測試結果放大,所以操作的次數要儘量設置大一點,實際在網站運行中程序代碼也會被成千上萬次運行,所以這麼做是可以接受的,也使得比較更明顯,並且這樣也可以減小某些偶然因素帶來的干擾。

因爲在ASP.NET中測試不穩定因素太多,所以這部分代碼是以控制檯程序來運行的,運行上面的代碼可得到如圖21-1所示的效果:

 

21-1 程序執行結果

在上面的代碼中我們是採用了指定ArrayListList<int>泛型集合的初始化容量大小,可以看出操作在集合元素固定的情況下,數組的操作是最快的,泛型集合的操作次之,ArrayList最慢。

以上測試是針對值類型數據的測試,如果是String這類的引用類型也會有類似的效果,只不過效果引用類型作爲集合元素沒有值類型作爲集合元素明顯。

21.1.2 字符串連接優化

.NET FrameworkString類是一個比較特殊的類,我們知道值類型變量直接在棧中分配內存來存儲變量的值,並且不需要垃圾回收器來回收,大部分引用類型變量是在堆中分配內存來存儲變量的值,在不再使用的情況下會被垃圾回收器回收所佔用的內存。String類型的變量雖然是引用類型變量(常用的賦值方式卻很類似於值類型變量的賦值方式,如string a=”123”),但是CLRCommon Language Runtime,通用語言運行時)通過了一種特殊的方法來存放字符串,CLR會維護一個會自動維護一個名爲“拘留池”(intern pool,不知道爲什麼微軟會這麼叫) 的表,它包含在程序中聲明的每個唯一字符串常數的單個實例,以及以編程方式添加的 String 的任何唯一實例。該拘留池節約字符串存儲區。如果將字符串常數分配給幾個變量,則每個變量設置爲引用“拘留池”(intern pool) 中的同一常數,而不是引用具有相同值的 String 的幾個不同實例。

看如下代碼:

String a=”abc”;

String b=”abc”;

在上面的代碼中變量a和變量b都指向了堆中的同一個引用,也就是和下面的代碼是等效的:

String a=”abc”;

String b=a;

在給字符串變量賦值時會首先在“拘留池”中檢查是否有與要賦值的值相等的字符串,如果存在就會返回該字符串的引用,如果不存在就向字符串“駐留池”中添加該字符串,並且將該字符串的引用返回。這樣一來在每次連接字符串時都有可能創建新的字符串對象(如果“駐留池”中不存在對應的字符串的話),從而導致了性能低下。

String類有個方法專門用來檢測“拘留池”中是否存在指定字符串引用的方法,這個方法就是IsInterned(string str)方法,如果存在這個引用則返回str的引用,如果不存在這個引用就返回null

在需要多次連接字符串時可以考慮使用System.Text.StringBuilder對象,這是一個可變容量的字符串對象。在實例化StringBuilder對象時會指定一個容量(如果不顯示指定,則系統默認會指定初始容量爲16,如果在程序中最終連接後的容量大於這個值可以自行指定一個較大的值作爲初時容量,這樣也能提高性能),在進行添加、插入及替換等修改操作時如果不超過容量,則會直接在緩衝區中操作,如果超過容量則會重新分配一個更大的緩衝區,並將原來的數據複製到新緩衝區。

下面通過一個控制檯的例子來演示一下String類和StringBuilder類的區別,代碼如下:

這個程序的運行效果如圖21-2所示:

 

21-2 String類和StringBuilder類連接字符串的運行效果

21.1.3 類型轉換優化

在開發中經常會遇到類型轉換的問題,一種情況是由字符串類型轉換成數值類型,另一種情況是存在繼承關係或者實現關係的類之間進行類型轉換。在上面的兩種轉換中如果存在不能轉換的情況,則會拋出異常,在引發和處理異常時將消耗大量的系統資源和執行時間。引發異常是爲了確實處理異常情況,而不是爲了處理可預知的時間或控制流(這一點尤其要注意,不要在代碼中來使用異常進行流程控制)。

21.1.3.1 字符串類型向值類型轉換

.NET Framework2.0版本以前將字符串類型轉換成數值類型都是使用Parse()方法,如int.Parse("123")char.Parse("a")bool.Parse("TrueString")等等,如果出現了指定的字符串不能轉換成相應的數值類型時就會拋出異常,可能會對性能造成不良的影響。在.NET Framework2.0及以後版本中增加了TryParse()方法,減小了性能問題。TryParse()方法使用了兩個參數:第一個參數是要轉換成數值類型的字符串,第二個參數是一個帶有out關鍵字的參數,並且這個方法有返回值,指示指定的字符串是否能轉換成相應的數據類型。如果指定的字符串能轉換成相應的數據類型則方法返回trueout參數就是指定字符串轉換成相應數值的結果,否則方法返回false,表示不能進行轉換而不會拋出異常。

其用法如下面的代碼所示:

 

21.1.3.2 引用類型之間轉換

在引用類型之間轉換有兩種方式:強制轉換和as轉換。下面是強制轉換的例子:

C#中還有一個is關鍵字,它用來檢查對象是否與給定類型兼容,如果兼容表達式的值爲true,否則爲false。例如下面的表達式:

假如有A類型的變量aB類型,對於A c=a as B這個轉換,存在如下情況:如果A實現或者派生自B,那麼上面的轉換成功,否則轉換不成功,cnull,並且不會拋出異常。

上面講到的都是關於編碼方面提高程序性能應該注意的實現,此外在編碼過程中還應注意儘量減少裝箱拆箱操作。裝箱操作是指將值類型轉換成引用類型,拆箱操作是指將引用類型轉換成值類型,通過裝箱操作使得值類型可以被視作對象。相對於簡單的賦值而言,裝箱和取消裝箱過程需要進行大量的計算。對值類型進行裝箱時,必須分配並構造一個全新的對象。次之,取消裝箱所需的強制轉換也需要進行大量的計算。在向ArrayList這樣的非範型集合中添加值類型元素時就會存在裝箱過程,再從非範型集合中取出值類型的值就會存在拆箱過程。

21.1.4 使用Server.Transfer()方法

使用Server.Transfer()方法實現同一應用程序下不同頁面間的重定向可以避免不必要的客戶端頁面重定向。它比Response.Redirect()方法性能要高,並且Server.Transfer()方法具有允許目標頁從源頁中讀取控件值和公共屬性值的優點。由於調用了這個方法之後瀏覽器上不會反應更改後的頁的信息,因此它也適合以隱藏URL的形式向用戶呈現頁面,不過如果用戶點擊了瀏覽器上的“後退“按鈕或者刷新頁面有可能導致意外情況。

21.1.5 避免不必要的服務器往返

雖然使用服務器控件能夠節省時間和代碼,但是使用服務器控件有時間會增加頁面的往返次數,如果在頁面中使用了數據綁定控件,在默認情況下每次響應客戶端回發而加載頁面時都會重新綁定數據,其實在很多情況下這個過程是沒有必要的,使用 Page.IsPostBack 避免對往返過程執行不必要的處理,這個在數據綁定控件那一章有所體現。

21.1.6 儘早釋放對象

.NET Framework中有很多類實現了IDisposable接口,實現了IDisposable接口的類中都會有一個Dispose()方法,當這些類的實例不再使用時,應及早調用該類的Dispose()方法以釋放所佔用的資源。

21.1.7 儘量減少服務器控件的使用

服務器控件在編程中使用起來確實方便,但是這種方便是犧牲了一定的性能爲前提的,比如需要在頁面某個地方顯示一個字符串,這個字符串在任何時候都不會發生變化,那麼可以在HTML代碼中直接輸出,還有有些表單要實現點擊按鈕之後清空表單輸入,利用HTML中的重置按鈕就可以完成這個功能,都沒有必要使用服務器控件。

21.2 數據操作優化

數據操作優化方面主要是數據訪問優化,主要有數據庫連接對象使用、數據訪問優化、優化SQL語句、使用緩存等。

21.2.1 數據庫連接對象使用優化

對於數據庫連接的使用始終遵循的一條原則是:儘可能晚打開數據庫連接,儘可能早關閉數據庫連接。這個在ADO.NET一章作過講述,再次不在贅述。

除此之外,還可以使用數據庫連接池來優化。連接到數據庫通常需要幾個需要很長時間的步驟組成,如建立物理通道(例如套接字或命名管道)、與服務器進行初次握手、分析連接字符串信息、由服務器對連接進行身份驗證、運行檢查以便在當前事務中登記等等。實際上,大多數應用程序僅使用一個或幾個不同的連接配置。這意味着在執行應用程序期間,許多相同的連接將反覆地打開和關閉。爲了使打開的連接成本最低,ADO.NET 使用稱爲連接池的優化方法。連接池減少新連接需要打開的次數。池進程保持物理連接的所有權。通過爲每個給定的連接配置保留一組活動連接來管理連接。只要用戶在連接上調用 Open,池進程就會檢查池中是否有可用的連接。如果某個池連接可用,會將該連接返回給調用者,而不是打開新連接。應用程序在該連接上調用 Close 時,池進程會將連接返回到活動連接池集中,而不是真正關閉連接。連接返回到池中之後,即可在下一個 Open 調用中重複使用。

池連接可以大大提高應用程序的性能和可縮放性。默認情況下,ADO.NET 中啓用連接池。除非顯式禁用,否則,連接在應用程序中打開和關閉時,池進程將對連接進行優化。在開發大型網站時可以更改默認的數據庫連接池配置信息,例如可以增加數據庫連接池的最大連接數(默認是100),如下面的代碼就是將數據庫連接池的最大連接數設爲200

Data Source=(local);Initial Catalog=AspNetStudy;User ID=sa;Password=sa;Pooling=true;Min Pool Size=0;Max Pool Size=200

當然也不是設置數據庫連接池的最大連接數越大越好,實際上還會受其它因素的限制。

21.2.2 數據訪問優化

如果對數據庫中的數據不是需要經常讀取,可以使用相應的DataReader對象來讀取(如SqlDataReaderOleDbDataReaderOracleDataReader),在這種情況下使用DataReader對象會得到一定的性能提升。

此外,在數據訪問時還可以使用存儲過程。使用存儲過程除了可以防範SQL注入之外,還可以提高程序性能和減少網絡流量。存儲過程是存儲在服務器上的一組預編譯的SQL語句,具有對數據庫立即訪問的功能,信息處理極爲迅速。使用存儲過程可以避免對命令的多次編譯,在執行一次後其執行規劃就駐留在高速緩存中,以後需要時只需直接調用緩存中的二進制代碼即可。

21.2.3 優化SQL語句

在開發中除了從C#代碼方面優化數據訪問之外,還可以從SQL語句上優化數據訪問。有人做過調查,在數據量大的庫中進行數據訪問,不同的人編寫的SQL語句所花費的時間有可能相差上百倍,因此儘量讓項目中對數據查詢優化有經驗的人編寫SQL語句以提高程序性能。

在優化SQL語句時,有幾條原則需要注意:

1)儘量避免”select * from 表名這樣的SQL語句,特別是在表中字段比較多而只需要顯示某幾個字段數據的情況下更應該注意這個問題,比如針對SQL Server數據庫來說,如果不需要顯示或者操作表中的imageTextntextxml這樣的字段,就儘量不要出現在select語句中的字段列表中。

2)儘量不要在查詢語句中使用子查詢。

3)儘量使用索引。索引是與表或視圖關聯的磁盤上結構,可以加快從表或視圖中檢索行的速度。索引包含由表或視圖中的一列或多列生成的鍵。這些鍵存儲在一個結構中,使數據庫可以快速有效地查找與鍵值關聯的行。設計良好的索引可以減少磁盤 I/O 操作,並且消耗的系統資源也較少,從而可以提高查詢性能。對於包含 SELECTUPDATE DELETE 語句的各種查詢,索引會很有用。查詢優化器使用索引時,搜索索引鍵列,查找到查詢所需行的存儲位置,然後從該位置提取匹配行。通常,搜索索引比搜索表要快很多,因爲索引與表不同,一般每行包含的列非常少,且行遵循排序順序。對於常用作where查詢的字段可以建立索引以提高查詢速度。注意,使用索引後會降低對錶的插入、更新和刪除速度,在一張表上也不宜建立過多的索引。

21.2.4 合理使用緩存

ASP.NET中在不同級別提供了緩存功能,比如控件級和頁面級及全局級都提供了緩存功能,在控件中或者頁面中都可以通過@ OutputCache指令來使用緩存,這對於減少一些不經常變化並且比較耗時的操作的性能損耗很有用。

除此之外,還有System.Web.Caching.Cache類對提高程序性能也非常有用,雖然利用Session或者Application也能實現在內存中保存數據,但是在Session中保存的數據只能被單個用戶使用,而在Application中使用的數據如果不手動釋放就會一直保存在內存當中,利用Cache就完全克服了上面的缺點。Cache類提供了強大的功能,允許自定義緩存項及緩存時間和優先級等,在服務器內存不夠用時會自動較少使用的或者優先級比較低的項以釋放內存。另外還可以指定緩存關聯依賴項,如果緩存關聯依賴項發生改變緩存項就會實效並從緩存中移除。比如可以將一個經常要讀取的文件的內容緩存起來,並在文件上保留一個依賴項,一旦文件內容發生變化就會從內存中移除緩存的文件內容,可以再次從文件中重新讀取文件內容到緩存中,這樣就保證了得到的文件內容是最新的。

下面就是一個在ASP.NET使用Cache的例子,頁面的設計部分代碼如下:

頁面的邏輯代碼如下:

 

爲了達到演示效果,還需要在頁面所在文件夾下創建一個txt文件,文件內容爲“這是《ASP.NET夜話》第二十一章中進行的Cache測試文件。”,並用UTF-8編碼保存,如圖21-3所示:

 

21-3 創建Cotent.txt文件並保存成UTF-8編碼

運行頁面之後的效果如圖21-4所示:

 

21-4 頁面的初始運行效果

點擊“顯示文件內容”按鈕,因爲首次顯示在緩存中並不存在文件內容,所以會將文件內容讀取出來並在文本框中顯示。在文本框顯示了文件內容之後,手動修改Content.txt文件的內容並保存,然後再次點擊顯示文件內容,在文本框中就會顯示文件的最新內容了,如圖21-5所示:

 

21-5 在修改文件內容之後重新顯示文件內容的效果

由此可見,使用了緩存關聯依賴項之後確實能移除緩存數據,下次顯示時因爲緩存項已經被移除所以會重新讀取文件內容並進行緩存,因而就能看到最新的文件內容。同時,還能在Content.txt文件所在文件夾下看到一個CacheChangeLog.txt文件,這個文件的內容如圖21-6所示:

 

21-6 CacheChangeLog.txt文件的內容

總之,Cache對象是一個使用起來很靈活的對象,可以滿足複雜條件下的數據緩存要求,合理使用緩存有時候能提高數量級的性能。不過在使用緩存時也要注意一些事項,比如不要緩存頻繁變化和很少使用的數據,也不要將數據緩存的時間設置過短,否則不但不能提高性能,嚴重情況下反而會降低性能。

21.3 配置優化

不光從程序代碼上能提高程序性能,調用某些設置也能提高程序的性能。

21.3.1 禁用調試模式

在開發過程中因爲經常要進行調試,所以配置將Web網站項目設置成允許調試模式,在部署網站時一定要禁用此模式,在運行過程中使用調試模式將會使網站的性能受到很大影響。禁用調試模式是在web.config文件中設置,如下面的代碼就是禁用調試模式:

<compilation debug="false">

21.3.2 合理使用ViewState

ASP.NET中爲了維護服務器控件在HTTP請求之間維護其狀態啓用了服務器控件的視圖狀態。服務器控件視圖狀態爲其所有屬性的累計值,這些值在後面的請求處理中作爲變量傳遞給隱藏的字段,一般情況下這些值是經過了一定的編碼或者加密處理之後再保存到隱藏字段中的,在後面的請求中再經過反向處理得到原始的值,這些處理都是需要花費時間的。有時候爲了提高應用程序的性能,在不需維護服務器控件的情況下可以禁用視圖狀態(默認情況下是啓用視圖狀態的),特別是在使用數據綁定控件時一定要注意這個問題。

下面是一個使用GridView控件來顯示數據的例子,這個文件其實在講述GridView控件時曾用來作爲例子,現在又再次用來作爲例子,代碼如下:

運行這個頁面會看到如圖21-7所示的效果。

 

21-7 GridView顯示用戶信息

如果看到這個頁面的最終生成的HTML頁面源代碼,會看到如圖21-8顯示的效果:

 

21-8 查看最終HTML頁面的源代碼效果

從上圖可以看出爲了維護GridView控件的狀態所花費的代價是客觀的,如果不需要維護這個狀態可以禁用GridView控件的視圖狀態(在大部分情況下都用不着)。具體操作是切換到設計視圖下,在頁面中選中GridView控件然後在屬性窗口中找到EnableViewState屬性並將其設爲false,如圖21-9所示:

 

21-9 禁用GridView控件的視圖狀態

禁用GridView的視圖狀態之後再次運行頁面並查看HTML源代碼,會看到如圖21-10所示的效果:

 

21-10 禁用GridView的視圖狀態之後生成的HTML源代碼

從圖21-8和圖21-10對比情況來看,禁用了視圖狀態之後生成的HTML代碼大小大大減少,不斷降低了網絡流量傳輸,還減輕了服務器的負擔。如果要禁用整個頁面的服務器控件的視圖狀態,可以在頁面@Page指令中添加EnableViewState="false"值,如本實例中頁面禁用頁面中所有控件視圖狀態之後,頁面的@Page指令如下:

<%@ Page Language="C#" EnableViewState="false" %>

最後要提示一點的是,禁用服務器控件的視圖狀態之後有可能有些服務器控件的內置功能無法使用,例如在本利中禁用GridView控件的視圖狀態之後就沒有辦法使用內置分頁功能了。

21.3.3 合理選擇會話狀態存儲方式

ASP.NET中支持多種會話狀態數據存儲方式,這是一個SessionStateMode枚舉值,如下表所示:

數據存儲模式

說明

InProc 模式

將會話狀態數據保存在ASP.NET進程中,只能用於單個服務器,Web服務器重其後不能保留會話狀態,這是默認的存儲方式

StateServer 模式

將會話狀態存儲在單獨的進程中,可將會話狀態用於多個Web服務器,並在Web服務器重啓後還能保留會話狀態

SQLServer 模式

會話數據保存到SQL Server 數據庫中,可將會話狀態用於多個Web服務器,並在Web服務器重啓後還能保留會話狀態

Custom 模式

自定義存儲方式

Off 模式

禁用會話狀態

上面的每種方式都有字節的優點和缺點,InProc 模式是存儲會話狀態最快的解決方案,如果不需要保存大量數據並且不需要Web服務器重其後還能保存數據,則建議使用這種方式。例如下面的設置就是用了使用了進程內存儲會話狀態,並且設置Session的超期時間爲20分鐘。

21.4 總結

上面提到的優化方法是筆者平時做性能優化時常用到的優化方法,除了上面的方法之外還可以通過使用更好的算法來提高程序性能。儘管優化的方法各異,但目的只有一個:今最大可能在滿足程序要求的情況下提高性能。除了上面的方法之外還有其它的方式,希望讀者朋友在開發和學習中自己多積累這方面的經驗。


說明,本篇是《ASP.NET夜話》第21章草稿,因爲寫作時間是2009年12月左右,當時還沒有出現ASP.NET4.0正式版和VS2010正式版,在它們出現之後有些地方略有些小變化。在本篇講得是從代碼和配置上提高性能,沒有講述如何使用集羣、負載均衡等方法來提高性能,因爲這超出了ASP.NET範圍之外。在這裏發表這篇文章主要是周公最近要講講利用工具來優化數據和代碼,這個只是作爲引子。

此外,上面提到的方法要注意其使用場合。

2010-06-24

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章