測試背景
在開發分詞元件時,需高頻率查找字串鍵值。網路上常見建議「Dictionary 優於 Hashtable 因避免裝箱拆箱」,但實測發現並非絕對。本文以 10 萬筆資料,三種字串長度 (16/128/1024),排序/未排序兩種插入順序,實測四大集合類型的插入與查找效能。
測試環境與方法
- 資料筆數:100,000 筆唯一字串
- 字串長度:16、128、1024 字元
- 插入順序:隨機順序 vs 已排序
- 測試項目:插入耗時、查找耗時 (單位:毫秒)
- 集合初始容量:預設 100,000 避免擴容干擾
插入效能測試結果 (ms)
| 字串長度 / 順序 | Hashtable | Dictionary | SortedDictionary | SortedList |
| 16 / 未排序 | 14 | 21 | 896 | 8,009 |
| 16 / 已排序 | 25 | 35 | 990 | 671 |
| 128 / 未排序 | 30 | 52 | 868 | 8,415 |
| 128 / 已排序 | 43 | 67 | 1,053 | 666 |
| 1024 / 未排序 | 132 | 262 | 1,269 | 8,159 |
| 1024 / 已排序 | 158 | 277 | 1,036 | 684 |
查找效能測試結果 (ms)
| 字串長度 / 順序 | Hashtable | Dictionary | SortedDictionary | SortedList |
| 16 / 未排序 | 13 | 15 | 412 | 366 |
| 16 / 已排序 | 25 | 29 | 349 | 315 |
| 128 / 未排序 | 31 | 40 | 492 | 438 |
| 128 / 已排序 | 42 | 54 | 408 | 371 |
| 1024 / 未排序 | 130 | 202 | 934 | 894 |
| 1024 / 已排序 | 160 | 219 | 801 | 757 |
深度分析
Hashtable 為何最快?
- **底層相同**:Dictionary 內部實際包裝 Hashtable,多一層泛型封裝開銷。
- **無裝箱誤區**:字串為參考型別,Hashtable 存儲 `object` 參考,不涉及值型別的裝箱拆箱。
- **字串比較優化**:.NET 字串駐留機制使相同內容字串指向同一記憶體,雜湊碼計算與相等比較極快 (僅比較參考位址)。
Dictionary 稍慢原因
- 泛型介面呼叫、型別安全檢查增加微小開銷。
- 長字串 (1024) 時差距明顯擴大 (查找 130ms vs 202ms),推測與泛型比較器呼叫有關。
樹狀結構 (SortedDictionary) 與陣列結構 (SortedList)
- **插入**:SortedDictionary 維護紅黑樹,O(log n);SortedList 隨機插入需大量陣列搬移,O(n),效能極差。
- **查找**:兩者皆 O(log n),但字串比較需逐字元比對大小 (非單純相等),開銷遠大於雜湊表的相等比較。
- **已排序優勢**:SortedList 依序插入時無需搬移,插入效能反超 SortedDictionary。
結論與選型建議
| 場景 | 首選 | 備選 | 理由 |
| 字串鍵值高頻查找、不需排序 |
Hashtable |
Dictionary |
效能最佳、記憶體效率高、無泛型封裝開銷 |
| 需型別安全、程式碼可讀性 |
Dictionary<string, T> |
Hashtable |
編譯時期型別檢查、IntelliSense 支援、現代慣用 |
| 需鍵值有序遍歷 |
SortedDictionary |
SortedList |
插入/刪除 O(log n) 穩定,適合頻繁修改 |
| 資料靜態、需索引存取、記憶體敏感 |
SortedList |
陣列 + Array.Sort |
記憶體緊湊、支援索引器、二分搜尋快 |
完整測試程式碼
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace CollectionBenchmark
{
class Program
{
static Random _rnd = new Random();
static Hashtable _ht;
static Dictionary _dict;
static SortedDictionary _sdict;
static SortedList _slist;
static string GetRandomString(int len)
{
var sb = new StringBuilder(len);
for (int i = 0; i < len; i++)
sb.Append((char)_rnd.Next(32, 128));
return sb.ToString();
}
static List GetTestData(int len, int count)
{
var list = new List(count);
for (int i = 0; i < count; i++) list.Add(GetRandomString(len));
return list;
}
static void TestInsert(List data, bool sorted)
{
if (sorted) data.Sort();
Console.WriteLine($"=== Insert: Len={data[0].Length}, Count={data.Count}, Sorted={sorted} ===");
var sw = Stopwatch.StartNew();
_ht = new Hashtable(data.Count);
foreach (var s in data) if (!_ht.ContainsKey(s)) _ht.Add(s, 0);
Console.WriteLine($"Hashtable: {sw.ElapsedMilliseconds} ms");
sw.Restart();
_dict = new Dictionary(data.Count);
foreach (var s in data) if (!_dict.ContainsKey(s)) _dict.Add(s, 0);
Console.WriteLine($"Dictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
_sdict = new SortedDictionary();
foreach (var s in data) if (!_sdict.ContainsKey(s)) _sdict.Add(s, 0);
Console.WriteLine($"SortedDictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
_slist = new SortedList(data.Count);
foreach (var s in data) if (!_slist.ContainsKey(s)) _slist.Add(s, 0);
Console.WriteLine($"SortedList: {sw.ElapsedMilliseconds} ms");
}
static void TestFind(List data, bool sorted)
{
Console.WriteLine($"=== Find: Len={data[0].Length}, Count={data.Count}, Sorted={sorted} ===");
var sw = Stopwatch.StartNew();
foreach (var s in data) if (_ht[s] == null) Console.WriteLine("Error!");
Console.WriteLine($"Hashtable: {sw.ElapsedMilliseconds} ms");
sw.Restart();
foreach (var s in data) if (!_dict.ContainsKey(s)) Console.WriteLine("Error!");
Console.WriteLine($"Dictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
foreach (var s in data) if (!_sdict.ContainsKey(s)) Console.WriteLine("Error!");
Console.WriteLine($"SortedDictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
foreach (var s in data) if (!_slist.ContainsKey(s)) Console.WriteLine("Error!");
Console.WriteLine($"SortedList: {sw.ElapsedMilliseconds} ms");
}
static void Main()
{
foreach (int len in new[] { 16, 128, 1024 })
{
var data = GetTestData(len, 100000);
TestInsert(data, false); TestFind(data, false);
TestInsert(data, true); TestFind(data, true);
}
Console.ReadKey();
}
}
}