欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

c# HashSet的擴(kuò)容機(jī)制需要注意的

 更新時間:2020年06月17日 17:02:33   作者:一線碼農(nóng)  
這篇文章主要介紹了c# HashSet的擴(kuò)容機(jī)制需要注意的兩個地方,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下

一:背景

1. 講故事

自從這個純內(nèi)存項目進(jìn)了大客戶之后,搞得我現(xiàn)在對內(nèi)存和CPU特別敏感,跑一點(diǎn)數(shù)據(jù)內(nèi)存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導(dǎo)致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼著也得會幾個花架子😄😄😄,廢話不多說,這一篇就來看看 HashSet 是如何擴(kuò)容的。

二:HashSet的擴(kuò)容機(jī)制

1. 如何查看

了解如何擴(kuò)容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點(diǎn)就是 HashSet.Add 方法。

從圖中可以看到最后的初始化是用 Initialize 的,而且里面有這么一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質(zhì)數(shù),哈哈,有點(diǎn)意思,什么叫質(zhì)數(shù)? 簡單說就是只能被 1 和 自身 整除的數(shù)就叫做質(zhì)數(shù),那好奇心就來了,一起看看質(zhì)數(shù)是怎么算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認(rèn)定義好了 72 個質(zhì)數(shù),最大的一個質(zhì)數(shù)是 719w,換句話就是說當(dāng)元素個數(shù)大于 719w 的時候,就只能使用 IsPrime 方法動態(tài)計算質(zhì)數(shù),如下代碼:

public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應(yīng)該明白了,當(dāng)你第一次Add的時候,默認(rèn)的空間占用是 72 個預(yù)定義中最小的一個質(zhì)數(shù) 3,看過我之前文章的朋友知道List的默認(rèn)大小是4,后面就是簡單粗暴的 * 2 處理,如下代碼。

private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴(kuò)容探究

當(dāng)HashSet的個數(shù)達(dá)到3之后,很顯然要進(jìn)行二次擴(kuò)容,這一點(diǎn)不像List用一個 EnsureCapacity 方法搞定就可以了,然后細(xì)看一下怎么擴(kuò)容。

public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最后的擴(kuò)容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質(zhì)數(shù),也就是 7 ,然后將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風(fēng)險了嗎?

<1> 時間上的風(fēng)險

為了方便演示,我把 72 個預(yù)定義的最后幾個質(zhì)數(shù)顯示出來。

public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當(dāng)HashSet的元素個數(shù)為 2893249 的時候觸發(fā)擴(kuò)容變成了 2893249 * 2 => 5786498 最接近的一個質(zhì)數(shù)為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。

    static void Main(string[] args)
    {
      var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

      hashSet.Add(int.MaxValue);

      Console.Read();
    }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
  LOCALS:
    0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:    System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:    64(0x40) bytes
File:    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
       MT  Field  Offset         Type VT   Attr      Value Name
00007ffaf0096d10 4000017    8    System.Int32[] 0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0 4000018    10 ...ivate.CoreLib]][] 0 instance 0000020e2bea1020 _slots
00007ffaeffdf828 4000019    28     System.Int32 1 instance     2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:    System.Int32[]
Size:    23997908(0x16e2dd4) bytes
Array:    Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None

而且最重要的是,這里是一次性擴(kuò)容的,而非像redis中實現(xiàn)的那樣漸進(jìn)式擴(kuò)容,時間開銷也是大家值得注意的

<2> 空間上的風(fēng)險

這個有什么風(fēng)險呢?可以看一下:289w 和 599w 兩個HashSet的占用空間大小,這也是我最敏感的。

    static void Main(string[] args)
    {
      var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

      var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
      hashSet2.Add(int.MaxValue);

      Console.Read();
    }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
  LOCALS:
    0x000000B1B4FEE4B8 = 0x000001d13363cc08
    0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的占用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的占用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費(fèi)了: 91.5 - 44.1 = 47.4M

如果你真以為僅僅浪費(fèi)了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴(kuò)容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直占用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去托管堆上抓一下 Slot[] m_slots int[] m_buckets 兩個數(shù)組,我把代碼修改如下:

  static void Main(string[] args)
  {
    var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
    hashSet2.Add(int.MaxValue);
    Console.Read();
  }


0:011> !dumpheap -stat
00007ffaf84f7ad0    3  123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這里就拿 Slot[] 說事,從上面代碼可以看到,托管堆上有三個 Slot[] 數(shù)組,這就有意思了,怎么有三個哈,是不是有點(diǎn)懵逼,沒關(guān)系,我們將三個 Slot[] 的地址找出來,一個一個看。

0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
     Address        MT   Size
0000016c91308048 00007ffaf84f7ad0 16743180   
0000016c928524b0 00007ffaf84f7ad0 34719012   
0000016ce9e61020 00007ffaf84f7ad0 71993676 

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
  0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
    rbp-18: 0000006afab7e618
      -> 0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
      -> 0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最后一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。

0:011> !do 0000016c91308048
Name:    System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:    16743180(0xff7b0c) bytes
Array:    Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:    System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:    34719012(0x211c524) bytes
Array:    Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:    System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:    71993676(0x44a894c) bytes
Array:    Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來托管堆不僅有擴(kuò)容前的Size :2893249,還有更前一次的擴(kuò)容Size: 1395263,所以按這種情況算: 托管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:托管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 ➕ 剛才的 47.4M 的空間虛占用,總浪費(fèi)為:118.5M,但愿我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中并沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用于將當(dāng)前Size擴(kuò)展到最接近的 質(zhì)數(shù) 值, 如下代碼所示:

public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應(yīng)用到本案例就是將 289w 限制到 347w,仍然有 58w的空間占用。 如下圖:

三: 總結(jié)

HashSet的時間和空間上虛占遠(yuǎn)比你想象的大很多,而且實占也不小,因為底層用到了雙數(shù)組 m_slots m_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; }所以了解完原理之后謹(jǐn)慎著用吧。

以上就是c# HashSet的擴(kuò)容機(jī)制需要注意的的詳細(xì)內(nèi)容,更多關(guān)于c# HashSet 擴(kuò)容機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Entity Framework主從表的增刪改

    Entity Framework主從表的增刪改

    這篇文章介紹了Entity Framework主從表的增刪改,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • C#實現(xiàn)用棧求逆序的方法示例

    C#實現(xiàn)用棧求逆序的方法示例

    這篇文章主要介紹了C#實現(xiàn)用棧求逆序的方法,涉及C#數(shù)據(jù)結(jié)構(gòu)中棧的壓入與取出相關(guān)操作技巧,需要的朋友可以參考下
    2017-07-07
  • 使用C#表達(dá)式樹實現(xiàn)對象的深克隆(實例詳解)

    使用C#表達(dá)式樹實現(xiàn)對象的深克隆(實例詳解)

    C# 的表達(dá)式樹提供了一個強(qiáng)大的機(jī)制,可以將代碼以數(shù)據(jù)結(jié)構(gòu)的形式表示出來,使得代碼可以在運(yùn)行時進(jìn)行檢查、修改或執(zhí)行,這為動態(tài)查詢生成、代碼優(yōu)化和動態(tài)編程提供了很多可能性,這篇文章主要介紹了使用C#強(qiáng)大的表達(dá)式樹實現(xiàn)對象的深克隆,需要的朋友可以參考下
    2024-05-05
  • C#中異步是4種實現(xiàn)方式詳解

    C#中異步是4種實現(xiàn)方式詳解

    這篇文章主要為大家詳細(xì)介紹了C#中異步實現(xiàn)方式的相關(guān)知識,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,有需要的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-03-03
  • C#實現(xiàn)順序隊列和鏈隊列的代碼實例

    C#實現(xiàn)順序隊列和鏈隊列的代碼實例

    今天小編就為大家分享一篇關(guān)于C#實現(xiàn)順序隊列和鏈隊列的代碼實例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2018-10-10
  • C# 使用modbus 讀取PLC 寄存器地址的方法

    C# 使用modbus 讀取PLC 寄存器地址的方法

    今天通過本文給大家介紹C# 使用modbus 讀取PLC 寄存器地址的方法,使用的組件Nmodbus,文中通過實例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2021-10-10
  • 使用C#在注冊表中保存信息的例子

    使用C#在注冊表中保存信息的例子

    最近做的項目需要在注冊表中記錄一些用戶設(shè)置,方便在程序下次啟動時讀取設(shè)置,應(yīng)用上次用戶保存的設(shè)置,挺簡單的
    2014-04-04
  • C# IP地址與整數(shù)之間轉(zhuǎn)換的具體方法

    C# IP地址與整數(shù)之間轉(zhuǎn)換的具體方法

    這篇文章介紹了C# IP地址與整數(shù)之間轉(zhuǎn)換的具體方法,有需要的朋友可以參考一下
    2013-10-10
  • C#顯示文件夾下所有圖片文件的方法

    C#顯示文件夾下所有圖片文件的方法

    這篇文章主要介紹了C#顯示文件夾下所有圖片文件的方法,涉及C#操作圖片文件的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-04-04
  • C#雙向鏈表LinkedList排序?qū)崿F(xiàn)方法

    C#雙向鏈表LinkedList排序?qū)崿F(xiàn)方法

    這篇文章主要介紹了C#雙向鏈表LinkedList排序?qū)崿F(xiàn)方法,涉及C#雙向鏈表的定義與排序技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-08-08

最新評論