Queue(佇列)是C#提供的FIFO(First In First Out)的Collection,最先進入的元素會最先被取出,Queue最基本的使用為Enqueue與Dequeue的Function,Enqueue為將資料塞入Queue中,Dequeue則從Queue中取出資料,另外,.Net還提供了Count, Contains, Peek...等Queue相關的屬性與操作讓開發者更容易使用。但這篇重點不是要來推銷Queue有多好用,這篇限量是要告訴大家一個提升Queue使用效率的小方法。
仔細到MSDN看看Queue Class的定義會發現到Queue的Constructor中,除了一般預設的無引數Constructor(Queue())外,和傳入集合的Constructor(Queue(ICollection)),另外還有一個傳入Capacity的Constructor(Queue(Int32)),與GrowFactor的Constructor(Queue(Int32, Single)),(備註:在泛型的Queue<T>中沒有GrowFactor的Constructor(Queue(Int32, Single)))。
一般來說,大家宣告Queue時都選擇使用預設的Constructor,往往不太清楚傳入Capacity的Constructor和預設的差異在哪,限量一開始以為Capacity為設定Queue的最大容量,可是每次測試超過最大容量時都會發現Queue的Size還是會動態成長,經過一番深究後,終於了解Capacity果然是容量沒錯,但它代表的不是最大容量,而是初始容量,限量使用.Net Reflector神器進入看Queue的Source Code,發現Queue Size是會動態增加的(詳見底下Queue.cs)。
一開始用預設Constructor時,Capacity預設為32,GrowFactor預設為2f,
Queue會產生一個長度為32的Array,在Enqueue時如果剛好Array滿了就會SetCapacity Function裡進行動態增加Size的動作,讓我們再進去SetCapacity Function裡面看看,它動態增加的方式是產生一個更大Size的Array,再把原本的Array用Copy的方式複製過去,這樣子複製的效能其實是不太好的,所以如果能夠在預先知道資料的最大數量,限量建議使用傳入Capacity的Constructor搭配GrowFactor,一來當Size小於Capacity時不會進行Array.Copy的動作,二來使用GrowFactor在增加Array Size時會以設定的GrowFactor倍數成長,大幅增加了執行的效能。
Queue.cs
接著限量要發揮研究生的實驗與驗證的精神,看測試看看Queue使用預設Constructor與Capacity的效能差異,以下為比較效能的Code:
QueueCompare.cs
QueueCompare中,限量寫了一個Round Function,使用Stopwatch測試兩種Queue在多次Enqueue完後的時間差異,每次實驗跑10 Round,讓我們看看實驗結果:
仔細到MSDN看看Queue Class的定義會發現到Queue的Constructor中,除了一般預設的無引數Constructor(Queue())外,和傳入集合的Constructor(Queue(ICollection)),另外還有一個傳入Capacity的Constructor(Queue(Int32)),與GrowFactor的Constructor(Queue(Int32, Single)),(備註:在泛型的Queue<T>中沒有GrowFactor的Constructor(Queue(Int32, Single)))。
一般來說,大家宣告Queue時都選擇使用預設的Constructor,往往不太清楚傳入Capacity的Constructor和預設的差異在哪,限量一開始以為Capacity為設定Queue的最大容量,可是每次測試超過最大容量時都會發現Queue的Size還是會動態成長,經過一番深究後,終於了解Capacity果然是容量沒錯,但它代表的不是最大容量,而是初始容量,限量使用.Net Reflector神器進入看Queue的Source Code,發現Queue Size是會動態增加的(詳見底下Queue.cs)。
一開始用預設Constructor時,Capacity預設為32,GrowFactor預設為2f,
Queue會產生一個長度為32的Array,在Enqueue時如果剛好Array滿了就會SetCapacity Function裡進行動態增加Size的動作,讓我們再進去SetCapacity Function裡面看看,它動態增加的方式是產生一個更大Size的Array,再把原本的Array用Copy的方式複製過去,這樣子複製的效能其實是不太好的,所以如果能夠在預先知道資料的最大數量,限量建議使用傳入Capacity的Constructor搭配GrowFactor,一來當Size小於Capacity時不會進行Array.Copy的動作,二來使用GrowFactor在增加Array Size時會以設定的GrowFactor倍數成長,大幅增加了執行的效能。
Queue.cs
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public Queue() : this(0x20, 2f) { } [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public Queue(int capacity) : this(capacity, 2f) { } public Queue(int capacity, float growFactor) { if (capacity < 0) { throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum")); } if ((growFactor < 1.0) || (growFactor > 10.0)) { throw new ArgumentOutOfRangeException("growFactor", Environment.GetResourceString("ArgumentOutOfRange_QueueGrowFactor", new object[] { 1, 10 })); } this._array = new object[capacity]; this._head = 0; this._tail = 0; this._size = 0; this._growFactor = (int) (growFactor * 100f); } … public virtual void Enqueue(object obj) { if (this._size == this._array.Length) { int capacity = (int) ((this._array.Length * this._growFactor) / 100L); if (capacity < (this._array.Length + 4)) { capacity = this._array.Length + 4; } this.SetCapacity(capacity); } this._array[this._tail] = obj; this._tail = (this._tail + 1) % this._array.Length; this._size++; this._version++; } … private void SetCapacity(int capacity) { object[] destinationArray = new object[capacity]; if (this._size > 0) { if (this._head < this._tail) { Array.Copy(this._array, this._head, destinationArray, 0, this._size); } else { Array.Copy(this._array, this._head, destinationArray, 0, this._array.Length - this._head); Array.Copy(this._array, 0, destinationArray, this._array.Length - this._head, this._tail); } } this._array = destinationArray; this._head = 0; this._tail = (this._size == capacity) ? 0 : this._size; this._version++; }
接著限量要發揮研究生的實驗與驗證的精神,看測試看看Queue使用預設Constructor與Capacity的效能差異,以下為比較效能的Code:
QueueCompare.cs
using System; using System.Collections; using System.Diagnostics; namespace ConsoleTest { class Program { static void Main( String[] args ) { for ( var i = 1 ; i <= 10 ; i++ ) { Console.WriteLine( String.Format( "Round {0}", i ) ); Round( 5000000, 10000000, 8f ); Console.WriteLine(); } Console.ReadLine(); } static void Round( Int32 count, Int32 capacity, Single growFactor ) { var watch = new Stopwatch(); var queue = new Queue(); watch.Start(); for ( var i = 0 ; i < count ; i++ ) { queue.Enqueue( i ); } watch.Stop(); Console.WriteLine( String.Format( "Queue: {0} ms", watch.ElapsedMilliseconds ) ); watch.Reset(); var queue2 = new Queue( capacity, growFactor ); watch.Start(); for ( var i = 0 ; i < count ; i++ ) { queue2.Enqueue( i ); } watch.Stop(); Console.WriteLine( String.Format( "Queue with initial capacity: {0} ms", watch.ElapsedMilliseconds ) ); } } }
QueueCompare中,限量寫了一個Round Function,使用Stopwatch測試兩種Queue在多次Enqueue完後的時間差異,每次實驗跑10 Round,讓我們看看實驗結果:
實驗一描述: 測試當Enqueue數量小於Capacity時兩個Queue的效能差異
實驗一參數設定:
參考來源:
MSDN - Queue類別
1. Count: 5000000
2. Capacity: 10000000
3. GrowFactor: 8f
實驗結果:
實驗結果:
實驗二描述: 測試當Enqueue數量大於Capacity時兩個Queue的效能差異
實驗二參數設定:
1. Count: 5000000
2. Capacity: 1000000
3. GrowFactor: 8f
實驗結果:
實驗結果:
實驗一的結果可以清楚的看到使用Capacity的Queue效能會比預設的還要好,而實驗二因為Size已經超過Capacity所以兩個Queue的效能差異時好時壞,所以實驗證明使用Capacity時效能會較好。
另外提一點,Queue又可分為一般Queue(針對Object為對象)與泛型Queue<T>(針對T為對象),在測試的時候,限量有使用過Queue<T>執行測試,發現使用Queue<T>執行動作會比一般的Queue還來的快,其中運作限量還沒仔細看清楚,所以就..............つづく (続く)。
參考來源:
MSDN - Queue類別
留言
張貼留言