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類別


留言
張貼留言