国产无遮挡裸体免费直播视频,久久精品国产蜜臀av,动漫在线视频一区二区,欧亚日韩一区二区三区,久艹在线 免费视频,国产精品美女网站免费,正在播放 97超级视频在线观看,斗破苍穹年番在线观看免费,51最新乱码中文字幕

C++高并發(fā)內存池的實現

 更新時間:2022年07月18日 11:28:58   作者:2021dragon  
本文主要介紹了C++高并發(fā)內存池的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

項目介紹

本項目實現的是一個高并發(fā)的內存池,它的原型是Google的一個開源項目tcmalloc,tcmalloc全稱Thread-Caching Malloc,即線程緩存的malloc,實現了高效的多線程內存管理,用于替換系統(tǒng)的內存分配相關函數malloc和free。

在這里插入圖片描述

tcmalloc的知名度也是非常高的,不少公司都在用它,比如Go語言就直接用它做了自己的內存分配器。

該項目就是把tcmalloc中最核心的框架簡化后拿出來,模擬實現出一個mini版的高并發(fā)內存池,目的就是學習tcmalloc的精華。

該項目主要涉及C/C++、數據結構(鏈表、哈希桶)、操作系統(tǒng)內存管理、單例模式、多線程、互斥鎖等方面的技術。

內存池介紹

池化技術

在說內存池之前,我們得先了解一下“池化技術”。所謂“池化技術”,就是程序先向系統(tǒng)申請過量的資源,然后自己進行管理,以備不時之需。

之所以要申請過量的資源,是因為申請和釋放資源都有較大的開銷,不如提前申請一些資源放入“池”中,當需要資源時直接從“池”中獲取,不需要時就將該資源重新放回“池”中即可。這樣使用時就會變得非??旖?,可以大大提高程序的運行效率。

在計算機中,有很多使用“池”這種技術的地方,除了內存池之外,還有連接池、線程池、對象池等。以服務器上的線程池為例,它的主要思想就是:先啟動若干數量的線程,讓它們處于睡眠狀態(tài),當接收到客戶端的請求時,喚醒池中某個睡眠的線程,讓它來處理客戶端的請求,當處理完這個請求后,線程又進入睡眠狀態(tài)。

內存池

內存池是指程序預先向操作系統(tǒng)申請一塊足夠大的內存,此后,當程序中需要申請內存的時候,不是直接向操作系統(tǒng)申請,而是直接從內存池中獲??;同理,當釋放內存的時候,并不是真正將內存返回給操作系統(tǒng),而是將內存返回給內存池。當程序退出時(或某個特定時間),內存池才將之前申請的內存真正釋放。

內存池主要解決的問題

內存池主要解決的就是效率的問題,它能夠避免讓程序頻繁的向系統(tǒng)申請和釋放內存。其次,內存池作為系統(tǒng)的內存分配器,還需要嘗試解決內存碎片的問題。

內存碎片分為內部碎片和外部碎片:

  • 外部碎片是一些空閑的小塊內存區(qū)域,由于這些內存空間不連續(xù),以至于合計的內存足夠,但是不能滿足一些內存分配申請需求。
  • 內部碎片是由于一些對齊的需求,導致分配出去的空間中一些內存無法被利用。

注意: 內存池嘗試解決的是外部碎片的問題,同時也盡可能的減少內部碎片的產生。

malloc

C/C++中我們要動態(tài)申請內存并不是直接去堆申請的,而是通過malloc函數去申請的,包括C++中的new實際上也是封裝了malloc函數的。

我們申請內存塊時是先調用malloc,malloc再去向操作系統(tǒng)申請內存。malloc實際就是一個內存池,malloc相當于向操作系統(tǒng)“批發(fā)”了一塊較大的內存空間,然后“零售”給程序用,當全部“售完”或程序有大量的內存需求時,再根據實際需求向操作系統(tǒng)“進貨”。

在這里插入圖片描述

malloc的實現方式有很多種,一般不同編譯器平臺用的都是不同的。比如Windows的VS系列中的malloc就是微軟自行實現的,而Linux下的gcc用的是glibc中的ptmalloc。

定長內存池的實現

malloc其實就是一個通用的內存池,在什么場景下都可以使用,但這也意味著malloc在什么場景下都不會有很高的性能,因為malloc并不是針對某種場景專門設計的。

定長內存池就是針對固定大小內存塊的申請和釋放的內存池,由于定長內存池只需要支持固定大小內存塊的申請和釋放,因此我們可以將其性能做到極致,并且在實現定長內存池時不需要考慮內存碎片等問題,因為我們申請/釋放的都是固定大小的內存塊。

我們可以通過實現定長內存池來熟悉一下對簡單內存池的控制,其次,這個定長內存池后面會作為高并發(fā)內存池的一個基礎組件。

如何實現定長?

在實現定長內存池時要做到“定長”有很多種方法,比如我們可以使用非類型模板參數,使得在該內存池中申請到的對象的大小都是N。

template<size_t N>
class ObjectPool
{};

此外,定長內存池也叫做對象池,在創(chuàng)建對象池時,對象池可以根據傳入的對象類型的大小來實現“定長”,因此我們可以通過使用模板參數來實現“定長”,比如創(chuàng)建定長內存池時傳入的對象類型是int,那么該內存池就只支持4字節(jié)大小內存的申請和釋放。

template<class T>
class ObjectPool
{};

如何直接向堆申請空間?

既然是內存池,那么我們首先得向系統(tǒng)申請一塊內存空間,然后對其進行管理。要想直接向堆申請內存空間,在Windows下,可以調用VirtualAlloc函數;在Linux下,可以調用brk或mmap函數。

#ifdef _WIN32
	#include <Windows.h>
#else
	//...
#endif

//直接去堆上申請按頁申請空間
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return ptr;
}

這里我們可以通過條件編譯將對應平臺下向堆申請內存的函數進行封裝,此后我們就不必再關心當前所在平臺,當我們需要直接向堆申請內存時直接調用我們封裝后的SystemAlloc函數即可。

定長內存池中應該包含哪些成員變量?

對于向堆申請到的大塊內存,我們可以用一個指針來對其進行管理,但僅用一個指針肯定是不夠的,我們還需要用一個變量來記錄這塊內存的長度。

由于此后我們需要將這塊內存進行切分,為了方便切分操作,指向這塊內存的指針最好是字符指針,因為指針的類型決定了指針向前或向后走一步有多大距離,對于字符指針來說,當我們需要向后移動n個字節(jié)時,直接對字符指針進行加n操作即可。

在這里插入圖片描述

其次,釋放回來的定長內存塊也需要被管理,我們可以將這些釋放回來的定長內存塊鏈接成一個鏈表,這里我們將管理釋放回來的內存塊的鏈表叫做自由鏈表,為了能找到這個自由鏈表,我們還需要一個指向自由鏈表的指針。

在這里插入圖片描述

因此,定長內存池當中包含三個成員變量:

  • _memory:指向大塊內存的指針。
  • _remainBytes:大塊內存切分過程中剩余字節(jié)數。
  • _freeList:還回來過程中鏈接的自由鏈表的頭指針。

內存池如何管理釋放的對象?

對于還回來的定長內存塊,我們可以用自由鏈表將其鏈接起來,但我們并不需要為其專門定義鏈式結構,我們可以讓內存塊的前4個字節(jié)(32位平臺)或8個字節(jié)(64位平臺)作為指針,存儲后面內存塊的起始地址即可。

因此在向自由鏈表插入被釋放的內存塊時,先讓該內存塊的前4個字節(jié)或8個字節(jié)存儲自由鏈表中第一個內存塊的地址,然后再讓_freeList指向該內存塊即可,也就是一個簡單的鏈表頭插操作。

在這里插入圖片描述

這里有一個有趣問題:如何讓一個指針在32位平臺下解引用后能向后訪問4個字節(jié),在64位平臺下解引用后能向后訪問8個字節(jié)?

首先我們得知道,32位平臺下指針的大小是4個字節(jié),64位平臺下指針的大小是8個字節(jié)。而指針指向數據的類型,決定了指針解引用后能向后訪問的空間大小,因此我們這里需要的是一個指向指針的指針,這里使用二級指針就行了。

當我們需要訪問一個內存塊的前4/8個字節(jié)時,我們就可以先該內存塊的地址先強轉為二級指針,由于二級指針存儲的是一級指針的地址,二級指針解引用能向后訪問一個指針的大小,因此在32位平臺下訪問的就是4個字節(jié),在64位平臺下訪問的就是8個字節(jié),此時我們訪問到了該內存塊的前4/8個字節(jié)。

void*& NextObj(void* ptr)
{
	return (*(void**)ptr);
}

需要注意的是,在釋放對象時,我們應該顯示調用該對象的析構函數清理該對象,因為該對象可能還管理著其他某些資源,如果不對其進行清理那么這些資源將無法被釋放,就會導致內存泄漏。

//釋放對象
void Delete(T* obj)
{
	//顯示調用T的析構函數清理對象
	obj->~T();

	//將釋放的對象頭插到自由鏈表
	NextObj(obj) = _freeList;
	_freeList = obj;
}

內存池如何為我們申請對象?

當我們申請對象時,內存池應該優(yōu)先把還回來的內存塊對象再次重復利用,因此如果自由鏈表當中有內存塊的話,就直接從自由鏈表頭刪一個內存塊進行返回即可。

在這里插入圖片描述

如果自由鏈表當中沒有內存塊,那么我們就在大塊內存中切出定長的內存塊進行返回,當內存塊切出后及時更新_memory指針的指向,以及_remainBytes的值即可。

在這里插入圖片描述

需要特別注意的是,由于當內存塊釋放時我們需要將內存塊鏈接到自由鏈表當中,因此我們必須保證切出來的對象至少能夠存儲得下一個地址,所以當對象的大小小于當前所在平臺指針的大小時,需要按指針的大小進行內存塊的切分。

此外,當大塊內存已經不足以切分出一個對象時,我們就應該調用我們封裝的SystemAlloc函數,再次向堆申請一塊內存空間,此時也要注意及時更新_memory指針的指向,以及_remainBytes的值。

//申請對象
T* New()
{
	T* obj = nullptr;

	//優(yōu)先把還回來的內存塊對象,再次重復利用
	if (_freeList != nullptr)
	{
		//從自由鏈表頭刪一個對象
		obj = (T*)_freeList;
		_freeList = NextObj(_freeList);
	}
	else
	{
		//保證對象能夠存儲得下地址
		size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
		//剩余內存不夠一個對象大小時,則重新開大塊空間
		if (_remainBytes < objSize)
		{
			_remainBytes = 128 * 1024;
			_memory = (char*)SystemAlloc(_remainBytes >> 13);
			if (_memory == nullptr)
			{
				throw std::bad_alloc();
			}
		}
		//從大塊內存中切出objSize字節(jié)的內存
		obj = (T*)_memory;
		_memory += objSize;
		_remainBytes -= objSize;
	}
	//定位new,顯示調用T的構造函數初始化
	new(obj)T;

	return obj;
}

需要注意的是,與釋放對象時需要顯示調用該對象的析構函數一樣,當內存塊切分出來后,我們也應該使用定位new,顯示調用該對象的構造函數對其進行初始化。

定長內存池整體代碼如下:

//定長內存池
template<class T>
class ObjectPool
{
public:
	//申請對象
	T* New()
	{
		T* obj = nullptr;

		//優(yōu)先把還回來的內存塊對象,再次重復利用
		if (_freeList != nullptr)
		{
			//從自由鏈表頭刪一個對象
			obj = (T*)_freeList;
			_freeList = NextObj(_freeList);
		}
		else
		{
			//保證對象能夠存儲得下地址
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			//剩余內存不夠一個對象大小時,則重新開大塊空間
			if (_remainBytes < objSize)
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			//從大塊內存中切出objSize字節(jié)的內存
			obj = (T*)_memory;
			_memory += objSize;
			_remainBytes -= objSize;
		}
		//定位new,顯示調用T的構造函數初始化
		new(obj)T;

		return obj;
	}
	//釋放對象
	void Delete(T* obj)
	{
		//顯示調用T的析構函數清理對象
		obj->~T();

		//將釋放的對象頭插到自由鏈表
		NextObj(obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory = nullptr;     //指向大塊內存的指針
	size_t _remainBytes = 0;     //大塊內存在切分過程中剩余字節(jié)數

	void* _freeList = nullptr;   //還回來過程中鏈接的自由鏈表的頭指針
};

性能對比

下面我們將實現的定長內存池和malloc/free進行性能對比,測試代碼如下:

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

void TestObjectPool()
{
	// 申請釋放的輪次
	const size_t Rounds = 3;
	// 每輪申請釋放多少次
	const size_t N = 1000000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);

	//malloc和free
	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}
	size_t end1 = clock();

	//定長內存池
	ObjectPool<TreeNode> TNPool;
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}

在代碼中,我們先用new申請若干個TreeNode對象,然后再用delete將這些對象再釋放,通過clock函數得到整個過程消耗的時間。(new和delete底層就是封裝的malloc和free)

然后再重復該過程,只不過將其中的new和delete替換為定長內存池當中的New和Delete,此時再通過clock函數得到該過程消耗的時間。

在這里插入圖片描述

可以看到在這個過程中,定長內存池消耗的時間比malloc/free消耗的時間要短。這就是因為malloc是一個通用的內存池,而定長內存池是專門針對申請定長對象而設計的,因此在這種特殊場景下定長內存池的效率更高,正所謂“尺有所短,寸有所長”。

高并發(fā)內存池整體框架設計

該項目解決的是什么問題?

現代很多的開發(fā)環(huán)境都是多核多線程,因此在申請內存的時,必然存在激烈的鎖競爭問題。malloc本身其實已經很優(yōu)秀了,但是在并發(fā)場景下可能會因為頻繁的加鎖和解鎖導致效率有所降低,而該項目的原型tcmalloc實現的就是一種在多線程高并發(fā)場景下更勝一籌的內存池。

在實現內存池時我們一般需要考慮到效率問題和內存碎片的問題,但對于高并發(fā)內存池來說,我們還需要考慮在多線程環(huán)境下的鎖競爭問題。

高并發(fā)內存池整體框架設計

在這里插入圖片描述

高并發(fā)內存池主要由以下三個部分構成:

  • thread cache: 線程緩存是每個線程獨有的,用于小于等于256KB的內存分配,每個線程獨享一個thread cache。
  • central cache: 中心緩存是所有線程所共享的,當thread cache需要內存時會按需從central cache中獲取內存,而當thread cache中的內存滿足一定條件時,central cache也會在合適的時機對其進行回收。
  • page cache: 頁緩存中存儲的內存是以頁為單位進行存儲及分配的,當central cache需要內存時,page cache會分配出一定數量的頁分配給central cache,而當central cache中的內存滿足一定條件時,page cache也會在合適的時機對其進行回收,并將回收的內存盡可能的進行合并,組成更大的連續(xù)內存塊,緩解內存碎片的問題。

進一步說明:

每個線程都有一個屬于自己的thread cache,也就意味著線程在thread cache申請內存時是不需要加鎖的,而一次性申請大于256KB內存的情況是很少的,因此大部分情況下申請內存時都是無鎖的,這也就是這個高并發(fā)內存池高效的地方。

每個線程的thread cache會根據自己的情況向central cache申請或歸還內存,這就避免了出現單個線程的thread cache占用太多內存,而其余thread cache出現內存吃緊的問題。

多線程的thread cache可能會同時找central cache申請內存,此時就會涉及線程安全的問題,因此在訪問central cache時是需要加鎖的,但central cache實際上是一個哈希桶的結構,只有當多個線程同時訪問同一個桶時才需要加鎖,所以這里的鎖競爭也不會很激烈。

各個部分的主要作用

thread cache主要解決鎖競爭的問題,每個線程獨享自己的thread cache,當自己的thread cache中有內存時該線程不會去和其他線程進行競爭,每個線程只要在自己的thread cache申請內存就行了。

central cache主要起到一個居中調度的作用,每個線程的thread cache需要內存時從central cache獲取,而當thread cache的內存多了就會將內存還給central cache,其作用類似于一個中樞,因此取名為中心緩存。

page cache就負責提供以頁為單位的大塊內存,當central cache需要內存時就會去向page cache申請,而當page cache沒有內存了就會直接去找系統(tǒng),也就是直接去堆上按頁申請內存塊。

threadcache

threadcache整體設計

定長內存池只支持固定大小內存塊的申請釋放,因此定長內存池中只需要一個自由鏈表管理釋放回來的內存塊?,F在我們要支持申請和釋放不同大小的內存塊,那么我們就需要多個自由鏈表來管理釋放回來的內存塊,因此thread cache實際上一個哈希桶結構,每個桶中存放的都是一個自由鏈表。

thread cache支持小于等于256KB內存的申請,如果我們將每種字節(jié)數的內存塊都用一個自由鏈表進行管理的話,那么此時我們就需要20多萬個自由鏈表,光是存儲這些自由鏈表的頭指針就需要消耗大量內存,這顯然是得不償失的。

這時我們可以選擇做一些平衡的犧牲,讓這些字節(jié)數按照某種規(guī)則進行對齊,例如我們讓這些字節(jié)數都按照8字節(jié)進行向上對齊,那么thread cache的結構就是下面這樣的,此時當線程申請1~8字節(jié)的內存時會直接給出8字節(jié),而當線程申請9~16字節(jié)的內存時會直接給出16字節(jié),以此類推。

在這里插入圖片描述

因此當線程要申請某一大小的內存塊時,就需要經過某種計算得到對齊后的字節(jié)數,進而找到對應的哈希桶,如果該哈希桶中的自由鏈表中有內存塊,那就從自由鏈表中頭刪一個內存塊進行返回;如果該自由鏈表已經為空了,那么就需要向下一層的central cache進行獲取了。

但此時由于對齊的原因,就可能會產生一些碎片化的內存無法被利用,比如線程只申請了6字節(jié)的內存,而thread cache卻直接給了8字節(jié)的內存,這多給出的2字節(jié)就無法被利用,導致了一定程度的空間浪費,這些因為某些對齊原因導致無法被利用的內存,就是內存碎片中的內部碎片。

鑒于當前項目比較復雜,我們最好對自由鏈表這個結構進行封裝,目前我們就提供Push和Pop兩個成員函數,對應的操作分別是將對象插入到自由鏈表(頭插)和從自由鏈表獲取一個對象(頭刪),后面在需要時還會添加對應的成員函數。

//管理切分好的小對象的自由鏈表
class FreeList
{
public:
	//將釋放的對象頭插到自由鏈表
	void Push(void* obj)
	{
		assert(obj);

		//頭插
		NextObj(obj) = _freeList;
		_freeList = obj;
	}

	//從自由鏈表頭部獲取一個對象
	void* Pop()
	{
		assert(_freeList);

		//頭刪
		void* obj = _freeList;
		_freeList = NextObj(_freeList);
		return obj;
	}

private:
	void* _freeList = nullptr; //自由鏈表
};

因此thread cache實際就是一個數組,數組中存儲的就是一個個的自由鏈表,至于這個數組中到底存儲了多少個自由鏈表,就需要看我們在進行字節(jié)數對齊時具體用的是什么映射對齊規(guī)則了。

threadcache哈希桶映射對齊規(guī)則

如何進行對齊?

上面已經說了,不是每個字節(jié)數都對應一個自由鏈表,這樣開銷太大了,因此我們需要制定一個合適的映射對齊規(guī)則。

首先,這些內存塊是會被鏈接到自由鏈表上的,因此一開始肯定是按8字節(jié)進行對齊是最合適的,因為我們必須保證這些內存塊,無論是在32位平臺下還是64位平臺下,都至少能夠存儲得下一個指針。

但如果所有的字節(jié)數都按照8字節(jié)進行對齊的話,那么我們就需要建立 256 × 1024 ÷ 8 = 32768 256\times1024\div8=32768 256×1024÷8=32768個桶,這個數量還是比較多的,實際上我們可以讓不同范圍的字節(jié)數按照不同的對齊數進行對齊,具體對齊方式如下:

字節(jié)數對齊數哈希桶下標
[ [ [ 1 , 128 1,128 1,128 ] ] ]8 8 8[ [ [ 0 , 16 ) 0,16) 0,16)
[ [ [ 128 + 1 , 1024 128+1,1024 128+1,1024 ] ] ]16 16 16[ [ [ 16 , 72 ) 16,72) 16,72)
[ [ [ 1024 + 1 , 8 × 1024 1024+1,8\times1024 1024+1,8×1024 ] ] ]128 128 128[ [ [ 72 , 128 ) 72,128) 72,128)
[ [ [ 8 × 1024 + 1 , 64 × 1024 8\times1024+1,64\times1024 8×1024+1,64×1024 ] ] ]1024 1024 1024[ [ [ 128 , 184 ) 128,184) 128,184)
[ [ [ 64 × 1024 + 1 , 256 × 1024 64\times1024+1,256\times1024 64×1024+1,256×1024 ] ] ]8 × 1024 8\times1024 8×1024[ [ [ 184 , 208 ) 184,208) 184,208)

空間浪費率

雖然對齊產生的內碎片會引起一定程度的空間浪費,但按照上面的對齊規(guī)則,我們可以將浪費率控制到百分之十左右。需要說明的是,1~128這個區(qū)間我們不做討論,因為1字節(jié)就算是對齊到2字節(jié)也有百分之五十的浪費率,這里我們就從第二個區(qū)間開始進行計算。

根據上面的公式,我們要得到某個區(qū)間的最大浪費率,就應該讓分子取到最大,讓分母取到最小。比如129~1024這個區(qū)間,該區(qū)域的對齊數是16,那么最大浪費的字節(jié)數就是15,而最小對齊后的字節(jié)數就是這個區(qū)間內的前16個數所對齊到的字節(jié)數,也就是144,那么該區(qū)間的最大浪費率也就是 15 ÷ 144 ≈ 10.42 % 15\div144\approx10.42\% 15÷144≈10.42%。同樣的道理,后面兩個區(qū)間的最大浪費率分別是 127 ÷ 1152 ≈ 11.02 % 127\div1152\approx11.02\% 127÷1152≈11.02%和 1023 ÷ 9216 ≈ 11.10 % 1023\div9216\approx11.10\% 1023÷9216≈11.10%。

對齊和映射相關函數的編寫

此時有了字節(jié)數的對齊規(guī)則后,我們就需要提供兩個對應的函數,分別用于獲取某一字節(jié)數對齊后的字節(jié)數,以及該字節(jié)數對應的哈希桶下標。關于處理對齊和映射的函數,我們可以將其封裝到一個類當中。

//管理對齊和映射等關系
class SizeClass
{
public:
	//獲取向上對齊后的字節(jié)數
	static inline size_t RoundUp(size_t bytes);
	//獲取對應哈希桶的下標
	static inline size_t Index(size_t bytes);
};

需要注意的是,SizeClass類當中的成員函數最好設置為靜態(tài)成員函數,否則我們在調用這些函數時就需要通過對象去調用,并且對于這些可能會頻繁調用的函數,可以考慮將其設置為內聯(lián)函數。

在獲取某一字節(jié)數向上對齊后的字節(jié)數時,可以先判斷該字節(jié)數屬于哪一個區(qū)間,然后再通過調用一個子函數進行進一步處理。

//獲取向上對齊后的字節(jié)數
static inline size_t RoundUp(size_t bytes)
{
	if (bytes <= 128)
	{
		return _RoundUp(bytes, 8);
	}
	else if (bytes <= 1024)
	{
		return _RoundUp(bytes, 16);
	}
	else if (bytes <= 8 * 1024)
	{
		return _RoundUp(bytes, 128);
	}
	else if (bytes <= 64 * 1024)
	{
		return _RoundUp(bytes, 1024);
	}
	else if (bytes <= 256 * 1024)
	{
		return _RoundUp(bytes, 8 * 1024);
	}
	else
	{
		assert(false);
		return -1;
	}
}

此時我們就需要編寫一個子函數,該子函數需要通過對齊數計算出某一字節(jié)數對齊后的字節(jié)數,最容易想到的就是下面這種寫法。

//一般寫法
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
	size_t alignSize = 0;
	if (bytes%alignNum != 0)
	{
		alignSize = (bytes / alignNum + 1)*alignNum;
	}
	else
	{
		alignSize = bytes;
	}
	return alignSize;
}

除了上述寫法,我們還可以通過位運算的方式來進行計算,雖然位運算可能并沒有上面的寫法容易理解,但計算機執(zhí)行位運算的速度是比執(zhí)行乘法和除法更快的。

//位運算寫法
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
	return ((bytes + alignNum - 1)&~(alignNum - 1));
}

對于上述位運算,我們以10字節(jié)按8字節(jié)對齊為例進行分析。 8 − 1 = 7 8-1=7 8−1=7,7就是一個低三位為1其余位為0的二進制序列,我們將10與7相加,相當于將10字節(jié)當中不夠8字節(jié)的剩余字節(jié)數補上了。

在這里插入圖片描述

然后我們再將該值與7按位取反后的值進行與運算,而7按位取反后是一個低三位為0其余位為1的二進制序列,該操作進行后相當于屏蔽了該值的低三位而該值的其余位保持不變,此時得到的值就是10字節(jié)按8字節(jié)對齊后的值,即16字節(jié)。

在這里插入圖片描述

在獲取某一字節(jié)數對應的哈希桶下標時,也是先判斷該字節(jié)數屬于哪一個區(qū)間,然后再通過調用一個子函數進行進一步處理。

//獲取對應哈希桶的下標
static inline size_t Index(size_t bytes)
{
	//每個區(qū)間有多少個自由鏈表
	static size_t groupArray[4] = { 16, 56, 56, 56 };
	if (bytes <= 128)
	{
		return _Index(bytes, 3);
	}
	else if (bytes <= 1024)
	{
		return _Index(bytes - 128, 4) + groupArray[0];
	}
	else if (bytes <= 8 * 1024)
	{
		return _Index(bytes - 1024, 7) + groupArray[0] + groupArray[1];
	}
	else if (bytes <= 64 * 1024)
	{
		return _Index(bytes - 8 * 1024, 10) + groupArray[0] + groupArray[1] + groupArray[2];
	}
	else if (bytes <= 256 * 1024)
	{
		return _Index(bytes - 64 * 1024, 13) + groupArray[0] + groupArray[1] + groupArray[2] + groupArray[3];
	}
	else
	{
		assert(false);
		return -1;
	}
}

此時我們需要編寫一個子函數來繼續(xù)進行處理,容易想到的就是根據對齊數來計算某一字節(jié)數對應的下標。

//一般寫法
static inline size_t _Index(size_t bytes, size_t alignNum)
{
	size_t index = 0;
	if (bytes%alignNum != 0)
	{
		index = bytes / alignNum;
	}
	else
	{
		index = bytes / alignNum - 1;
	}
	return index;
}

當然,為了提高效率下面也提供了一個用位運算來解決的方法,需要注意的是,此時我們并不是傳入該字節(jié)數的對齊數,而是將對齊數寫成2的n次方的形式后,將這個n值進行傳入。比如對齊數是8,傳入的就是3。

//位運算寫法
static inline size_t _Index(size_t bytes, size_t alignShift)
{
	return ((bytes + (1 << alignShift) - 1) >> alignShift) - 1;
}

這里我們還是以10字節(jié)按8字節(jié)對齊為例進行分析。此時傳入的alignShift就是3,將1左移3位后得到的實際上就是對齊數8, 8 − 1 = 7 8-1=7 8−1=7,相當于我們還是讓10與7相加。

在這里插入圖片描述

之后我們再將該值向右移3位,實際上就是讓這個值除以8,此時我們也是相當于屏蔽了該值二進制的低三位,因為除以8得到的值與其二進制的低三位無關,所以我們可以說是將10對齊后的字節(jié)數除以了8,此時得到了2,而最后還需要減一是因為數組的下標是從0開始的。

ThreadCache類

按照上述的對齊規(guī)則,thread cache中桶的個數,也就是自由鏈表的個數是208,以及thread cache允許申請的最大內存大小256KB,我們可以將這些數據按照如下方式進行定義。

//小于等于MAX_BYTES,就找thread cache申請
//大于MAX_BYTES,就直接找page cache或者系統(tǒng)堆申請
static const size_t MAX_BYTES = 256 * 1024;
//thread cache和central cache自由鏈表哈希桶的表大小
static const size_t NFREELISTS = 208;

現在就可以對ThreadCache類進行定義了,thread cache就是一個存儲208個自由鏈表的數組,目前thread cache就先提供一個Allocate函數用于申請對象就行了,后面需要時再進行增加。

class ThreadCache
{
public:
	//申請內存對象
	void* Allocate(size_t size);

private:
	FreeList _freeLists[NFREELISTS]; //哈希桶
};

在thread cache申請對象時,通過所給字節(jié)數計算出對應的哈希桶下標,如果桶中自由鏈表不為空,則從該自由鏈表中取出一個對象進行返回即可;但如果此時自由鏈表為空,那么我們就需要從central cache進行獲取了,這里的FetchFromCentralCache函數也是thread cache類中的一個成員函數,在后面再進行具體實現。

//申請內存對象
void* ThreadCache::Allocate(size_t size)
{
	assert(size <= MAX_BYTES);
	size_t alignSize = SizeClass::RoundUp(size);
	size_t index = SizeClass::Index(size);
	if (!_freeLists[index].Empty())
	{
		return _freeLists[index].Pop();
	}
	else
	{
		return FetchFromCentralCache(index, alignSize);
	}
}

threadcacheTLS無鎖訪問

每個線程都有一個自己獨享的thread cache,那應該如何創(chuàng)建這個thread cache呢?我們不能將這個thread cache創(chuàng)建為全局的,因為全局變量是所有線程共享的,這樣就不可避免的需要鎖來控制,增加了控制成本和代碼復雜度。

要實現每個線程無鎖的訪問屬于自己的thread cache,我們需要用到線程局部存儲TLS(Thread Local Storage),這是一種變量的存儲方法,使用該存儲方法的變量在它所在的線程是全局可訪問的,但是不能被其他線程訪問到,這樣就保持了數據的線程獨立性。

//TLS - Thread Local Storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

但不是每個線程被創(chuàng)建時就立馬有了屬于自己的thread cache,而是當該線程調用相關申請內存的接口時才會創(chuàng)建自己的thread cache,因此在申請內存的函數中會包含以下邏輯。

//通過TLS,每個線程無鎖的獲取自己專屬的ThreadCache對象
if (pTLSThreadCache == nullptr)
{
	pTLSThreadCache = new ThreadCache;
}

centralcache

centralcache整體設計

當線程申請某一大小的內存時,如果thread cache中對應的自由鏈表不為空,那么直接取出一個內存塊進行返回即可,但如果此時該自由鏈表為空,那么這時thread cache就需要向central cache申請內存了。

central cache的結構與thread cache是一樣的,它們都是哈希桶的結構,并且它們遵循的對齊映射規(guī)則都是一樣的。這樣做的好處就是,當thread cache的某個桶中沒有內存了,就可以直接到central cache中對應的哈希桶里去取內存就行了。

central cache與thread cache的不同之處

central cache與thread cache有兩個明顯不同的地方,首先,thread cache是每個線程獨享的,而central cache是所有線程共享的,因為每個線程的thread cache沒有內存了都會去找central cache,因此在訪問central cache時是需要加鎖的。

但central cache在加鎖時并不是將整個central cache全部鎖上了,central cache在加鎖時用的是桶鎖,也就是說每個桶都有一個鎖。此時只有當多個線程同時訪問central cache的同一個桶時才會存在鎖競爭,如果是多個線程同時訪問central cache的不同桶就不會存在鎖競爭。

central cache與thread cache的第二個不同之處就是,thread cache的每個桶中掛的是一個個切好的內存塊,而central cache的每個桶中掛的是一個個的span。

在這里插入圖片描述

每個span管理的都是一個以頁為單位的大塊內存,每個桶里面的若干span是按照雙鏈表的形式鏈接起來的,并且每個span里面還有一個自由鏈表,這個自由鏈表里面掛的就是一個個切好了的內存塊,根據其所在的哈希桶這些內存塊被切成了對應的大小。

centralcache結構設計

頁號的類型?

每個程序運行起來后都有自己的進程地址空間,在32位平臺下,進程地址空間的大小是232;而在64位平臺下,進程地址空間的大小就是264。

頁的大小一般是4K或者8K,我們以8K為例。在32位平臺下,進程地址空間就可以被分成 2 32 ÷ 2 13 = 2 19 2^{32}\div2^{13}=2^{19} 232÷213=219個頁;在64位平臺下,進程地址空間就可以被分成 2 64 ÷ 2 13 = 2 51 2^{64}\div2^{13}=2^{51} 264÷213=251個頁。頁號本質與地址是一樣的,它們都是一個編號,只不過地址是以一個字節(jié)為一個單位,而頁是以多個字節(jié)為一個單位。

由于頁號在64位平臺下的取值范圍是 [ [ [ 0 , 2 51 ) 0,2^{51}) 0,251),因此我們不能簡單的用一個無符號整型來存儲頁號,這時我們需要借助條件編譯來解決這個問題。

#ifdef _WIN64
	typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#else
	//linux
#endif

需要注意的是,在32位下,_WIN32有定義,_WIN64沒有定義;而在64位下,_WIN32和_WIN64都有定義。因此在條件編譯時,我們應該先判斷_WIN64是否有定義,再判斷_WIN32是否有定義。

span的結構

central cache的每個桶里掛的是一個個的span,span是一個管理以頁為單位的大塊內存,span的結構如下:

//管理以頁為單位的大塊內存
struct Span
{
	PAGE_ID _pageId = 0;        //大塊內存起始頁的頁號
	size_t _n = 0;              //頁的數量

	Span* _next = nullptr;      //雙鏈表結構
	Span* _prev = nullptr;

	size_t _useCount = 0;       //切好的小塊內存,被分配給thread cache的計數
	void* _freeList = nullptr;  //切好的小塊內存的自由鏈表
};

對于span管理的以頁為單位的大塊內存,我們需要知道這塊內存具體在哪一個位置,便于之后page cache進行前后頁的合并,因此span結構當中會記錄所管理大塊內存起始頁的頁號。

至于每一個span管理的到底是多少個頁,這并不是固定的,需要根據多方面的因素來控制,因此span結構當中有一個_n成員,該成員就代表著該span管理的頁的數量。

此外,每個span管理的大塊內存,都會被切成相應大小的內存塊掛到當前span的自由鏈表中,比如8Byte哈希桶中的span,會被切成一個個8Byte大小的內存塊掛到當前span的自由鏈表中,因此span結構中需要存儲切好的小塊內存的自由鏈表。

span結構當中的_useCount成員記錄的就是,當前span中切好的小塊內存,被分配給thread cache的計數,當某個span的_useCount計數變?yōu)?時,代表當前span切出去的內存塊對象全部還回來了,此時central cache就可以將這個span再還給page cache。

每個桶當中的span是以雙鏈表的形式組織起來的,當我們需要將某個span歸還給page cache時,就可以很方便的將該span從雙鏈表結構中移出。如果用單鏈表結構的話就比較麻煩了,因為單鏈表在刪除時,需要知道當前結點的前一個結點。

雙鏈表結構

根據上面的描述,central cache的每個哈希桶里面存儲的都是一個雙鏈表結構,對于該雙鏈表結構我們可以對其進行封裝。

//帶頭雙向循環(huán)鏈表
class SpanList
{
public:
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);

		Span* prev = pos->_prev;

		prev->_next = newSpan;
		newSpan->_prev = prev;

		newSpan->_next = pos;
		pos->_prev = newSpan;
	}
	void Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head); //不能刪除哨兵位的頭結點

		Span* prev = pos->_prev;
		Span* next = pos->_next;

		prev->_next = next;
		next->_prev = prev;
	}
private:
	Span* _head;
public:
	std::mutex _mtx; //桶鎖
};

需要注意的是,從雙鏈表刪除的span會還給下一層的page cache,相當于只是把這個span從雙鏈表中移除,因此不需要對刪除的span進行delete操作。

central cache的結構

central cache的映射規(guī)則和thread cache是一樣的,因此central cache里面哈希桶的個數也是208,但central cache每個哈希桶中存儲就是我們上面定義的雙鏈表結構。

class CentralCache
{
public:
	//...
private:
	SpanList _spanLists[NFREELISTS];
};

central cache和thread cache的映射規(guī)則一樣,有一個好處就是,當thread cache的某個桶沒有內存了,就可以直接去central cache對應的哈希桶進行申請就行了。

centralcache核心實現

central cache的實現方式

每個線程都有一個屬于自己的thread cache,我們是用TLS來實現每個線程無鎖的訪問屬于自己的thread cache的。而central cache和page cache在整個進程中只有一個,對于這種只能創(chuàng)建一個對象的類,我們可以將其設置為單例模式。

單例模式可以保證系統(tǒng)中該類只有一個實例,并提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。單例模式又分為餓漢模式和懶漢模式,懶漢模式相對較復雜,我們這里使用餓漢模式就足夠了。

//單例模式
class CentralCache
{
public:
	//提供一個全局訪問點
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}
private:
	SpanList _spanLists[NFREELISTS];
private:
	CentralCache() //構造函數私有
	{}
	CentralCache(const CentralCache&) = delete; //防拷貝

	static CentralCache _sInst;
};

為了保證CentralCache類只能創(chuàng)建一個對象,我們需要將central cache的構造函數和拷貝構造函數設置為私有,或者在C++11中也可以在函數聲明的后面加上=delete進行修飾。

CentralCache類當中還需要有一個CentralCache類型的靜態(tài)的成員變量,當程序運行起來后我們就立馬創(chuàng)建該對象,在此后的程序中就只有這一個單例了。

CentralCache CentralCache::_sInst;

最后central cache還需要提供一個公有的成員函數,用于獲取該對象,此時在整個進程中就只會有一個central cache對象了。

慢開始反饋調節(jié)算法

當thread cache向central cache申請內存時,central cache應該給出多少個對象呢?這是一個值得思考的問題,如果central cache給的太少,那么thread cache在短時間內用完了又會來申請;但如果一次性給的太多了,可能thread cache用不完也就浪費了。

鑒于此,我們這里采用了一個慢開始反饋調節(jié)算法。當thread cache向central cache申請內存時,如果申請的是較小的對象,那么可以多給一點,但如果申請的是較大的對象,就可以少給一點。

通過下面這個函數,我們就可以根據所需申請的對象的大小計算出具體給出的對象個數,并且可以將給出的對象個數控制到2~512個之間。也就是說,就算thread cache要申請的對象再小,我最多一次性給出512個對象;就算thread cache要申請的對象再大,我至少一次性給出2個對象。

//管理對齊和映射等關系
class SizeClass
{
public:
	//thread cache一次從central cache獲取對象的上限
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);
	
		//對象越小,計算出的上限越高
		//對象越大,計算出的上限越低
		int num = MAX_BYTES / size;
		if (num < 2)
			num = 2;
		if (num > 512)
			num = 512;
	
		return num;
	}
};

但就算申請的是小對象,一次性給出512個也是比較多的,基于這個原因,我們可以在FreeList結構中增加一個叫做_maxSize的成員變量,該變量的初始值設置為1,并且提供一個公有成員函數用于獲取這個變量。也就是說,現在thread cache中的每個自由鏈表都會有一個自己的_maxSize。

//管理切分好的小對象的自由鏈表
class FreeList
{
public:
	size_t& MaxSize()
	{
		return _maxSize;
	}

private:
	void* _freeList = nullptr; //自由鏈表
	size_t _maxSize = 1;
};

此時當thread cache申請對象時,我們會比較_maxSize和計算得出的值,取出其中的較小值作為本次申請對象的個數。此外,如果本次采用的是_maxSize的值,那么還會將thread cache中該自由鏈表的_maxSize的值進行加一。

因此,thread cache第一次向central cache申請某大小的對象時,申請到的都是一個,但下一次thread cache再向central cache申請同樣大小的對象時,因為該自由鏈表中的_maxSize增加了,最終就會申請到兩個。直到該自由鏈表中_maxSize的值,增長到超過計算出的值后就不會繼續(xù)增長了,此后申請到的對象個數就是計算出的個數。(這有點像網絡中擁塞控制的機制)

從中心緩存獲取對象

每次thread cache向central cache申請對象時,我們先通過慢開始反饋調節(jié)算法計算出本次應該申請的對象的個數,然后再向central cache進行申請。

如果thread cache最終申請到對象的個數就是一個,那么直接將該對象返回即可。為什么需要返回一個申請到的對象呢?因為thread cache要向central cache申請對象,其實由于某個線程向thread cache申請對象但thread cache當中沒有,這才導致thread cache要向central cache申請對象。因此central cache將對象返回給thread cache后,thread cache會再將該對象返回給申請對象的線程。

但如果thread cache最終申請到的是多個對象,那么除了將第一個對象返回之外,還需要將剩下的對象掛到thread cache對應的哈希桶當中。

//從中心緩存獲取對象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	//慢開始反饋調節(jié)算法
	//1、最開始不會一次向central cache一次批量要太多,因為要太多了可能用不完
	//2、如果你不斷有size大小的內存需求,那么batchNum就會不斷增長,直到上限
	size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
	if (batchNum == _freeLists[index].MaxSize())
	{
		_freeLists[index].MaxSize() += 1;
	}
	void* start = nullptr;
	void* end = nullptr;
	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
	assert(actualNum >= 1); //至少有一個

	if (actualNum == 1) //申請到對象的個數是一個,則直接將這一個對象返回即可
	{
		assert(start == end);
		return start;
	}
	else //申請到對象的個數是多個,還需要將剩下的對象掛到thread cache中對應的哈希桶中
	{
		_freeLists[index].PushRange(NextObj(start), end);
		return start;
	}
}

從中心緩存獲取一定數量的對象

這里我們要從central cache獲取n個指定大小的對象,這些對象肯定都是從central cache對應哈希桶的某個span中取出來的,因此取出來的這n個對象是鏈接在一起的,我們只需要得到這段鏈表的頭和尾即可,這里可以采用輸出型參數進行獲取。

//從central cache獲取一定數量的對象給thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t n, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock(); //加鎖
	
	//在對應哈希桶中獲取一個非空的span
	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span); //span不為空
	assert(span->_freeList); //span當中的自由鏈表也不為空

	//從span中獲取n個對象
	//如果不夠n個,有多少拿多少
	start = span->_freeList;
	end = span->_freeList;
	size_t actualNum = 1;
	while (NextObj(end)&&n - 1)
	{
		end = NextObj(end);
		actualNum++;
		n--;
	}
	span->_freeList = NextObj(end); //取完后剩下的對象繼續(xù)放到自由鏈表
	NextObj(end) = nullptr; //取出的一段鏈表的表尾置空
	span->_useCount += actualNum; //更新被分配給thread cache的計數

	_spanLists[index]._mtx.unlock(); //解鎖
	return actualNum;
}

由于central cache是所有線程共享的,所以我們在訪問central cache中的哈希桶時,需要先給對應的哈希桶加上桶鎖,在獲取到對象后再將桶鎖解掉。

在向central cache獲取對象時,先是在central cache對應的哈希桶中獲取到一個非空的span,然后從這個span的自由鏈表中取出n個對象即可,但可能這個非空的span的自由鏈表當中對象的個數不足n個,這時該自由鏈表當中有多少個對象就給多少就行了。

也就是說,thread cache實際從central cache獲得的對象的個數可能與我們傳入的n值是不一樣的,因此我們需要統(tǒng)計本次申請過程中,實際thread cache獲取到的對象個數,然后根據該值及時更新這個span中的小對象被分配給thread cache的計數。

需要注意的是,雖然我們實際申請到對象的個數可能比n要小,但這并不會產生任何影響。因為thread cache的本意就是向central cache申請一個對象,我們之所以要一次多申請一些對象,是因為這樣一來下次線程再申請相同大小的對象時就可以直接在thread cache里面獲取了,而不用再向central cache申請對象。

插入一段范圍的對象到自由鏈表

此外,如果thread cache最終從central cache獲取到的對象個數是大于一的,那么我們還需要將剩下的對象插入到thread cache中對應的哈希桶中,為了能讓自由鏈表支持插入一段范圍的對象,我們還需要在FreeList類中增加一個對應的成員函數。

//管理切分好的小對象的自由鏈表
class FreeList
{
public:
	//插入一段范圍的對象到自由鏈表
	void PushRange(void* start, void* end)
	{
		assert(start);
		assert(end);

		//頭插
		NextObj(end) = _freeList;
		_freeList = start;
	}
private:
	void* _freeList = nullptr; //自由鏈表
	size_t _maxSize = 1;
};

pagecache

pagecache整體設計

page cache與central cache結構的相同之處

page cache與central cache一樣,它們都是哈希桶的結構,并且page cache的每個哈希桶中里掛的也是一個個的span,這些span也是按照雙鏈表的結構鏈接起來的。

page cache與central cache結構的不同之處

首先,central cache的映射規(guī)則與thread cache保持一致,而page cache的映射規(guī)則與它們都不相同。page cache的哈希桶映射規(guī)則采用的是直接定址法,比如1號桶掛的都是1頁的span,2號桶掛的都是2頁的span,以此類推。

其次,central cache每個桶中的span被切成了一個個對應大小的對象,以供thread cache申請。而page cache當中的span是沒有被進一步切小的,因為page cache服務的是central cache,當central cache沒有span時,向page cache申請的是某一固定頁數的span,而如何切分申請到的這個span就應該由central cache自己來決定。

在這里插入圖片描述

至于page cache當中究竟有多少個桶,這就要看你最大想掛幾頁的span了,這里我們就最大掛128頁的span,為了讓桶號與頁號對應起來,我們可以將第0號桶空出來不用,因此我們需要將哈希桶的個數設置為129。

//page cache中哈希桶的個數
static const size_t NPAGES = 129;

為什么這里最大掛128頁的span呢?因為線程申請單個對象最大是256KB,而128頁可以被切成4個256KB的對象,因此是足夠的。當然,如果你想在page cache中掛更大的span也是可以的,根據具體的需求進行設置就行了。

在page cache獲取一個n頁的span的過程

如果central cache要獲取一個n頁的span,那我們就可以在page cache的第n號桶中取出一個span返回給central cache即可,但如果第n號桶中沒有span了,這時我們并不是直接轉而向堆申請一個n頁的span,而是要繼續(xù)在后面的桶當中尋找span。

直接向堆申請以頁為單位的內存時,我們應該盡量申請大塊一點的內存塊,因為此時申請到的內存是連續(xù)的,當線程需要內存時我們可以將其切小后分配給線程,而當線程將內存釋放后我們又可以將其合并成大塊的連續(xù)內存。如果我們向堆申請內存時是小塊小塊的申請的,那么我們申請到的內存就不一定是連續(xù)的了。

因此,當第n號桶中沒有span時,我們可以繼續(xù)找第n+1號桶,因為我們可以將n+1頁的span切分成一個n頁的span和一個1頁的span,這時我們就可以將n頁的span返回,而將切分后1頁的span掛到1號桶中。但如果后面的桶當中都沒有span,這時我們就只能向堆申請一個128頁的內存塊,并將其用一個span結構管理起來,然后將128頁的span切分成n頁的span和128-n頁的span,其中n頁的span返回給central cache,而128-n頁的span就掛到第128-n號桶中。

也就是說,我們每次向堆申請的都是128頁大小的內存塊,central cache要的這些span實際都是由128頁的span切分出來的。

page cache的實現方式

當每個線程的thread cache沒有內存時都會向central cache申請,此時多個線程的thread cache如果訪問的不是central cache的同一個桶,那么這些線程是可以同時進行訪問的。這時central cache的多個桶就可能同時向page cache申請內存的,所以page cache也是存在線程安全問題的,因此在訪問page cache時也必須要加鎖。

但是在page cache這里我們不能使用桶鎖,因為當central cache向page cache申請內存時,page cache可能會將其他桶當中大頁的span切小后再給central cache。此外,當central cache將某個span歸還給page cache時,page cache也會嘗試將該span與其他桶當中的span進行合并。

也就是說,在訪問page cache時,我們可能需要訪問page cache中的多個桶,如果page cache用桶鎖就會出現大量頻繁的加鎖和解鎖,導致程序的效率低下。因此我們在訪問page cache時使用沒有使用桶鎖,而是用一個大鎖將整個page cache給鎖住。

而thread cache在訪問central cache時,只需要訪問central cache中對應的哈希桶就行了,因為central cache的每個哈希桶中的span都被切分成了對應大小,thread cache只需要根據自己所需對象的大小訪問central cache中對應的哈希桶即可,不會訪問其他哈希桶,因此central cache可以用桶鎖。

此外,page cache在整個進程中也是只能存在一個的,因此我們也需要將其設置為單例模式。

//單例模式
class PageCache
{
public:
	//提供一個全局訪問點
	static PageCache* GetInstance()
	{
		return &_sInst;
	}
private:
	SpanList _spanLists[NPAGES];
	std::mutex _pageMtx; //大鎖
private:
	PageCache() //構造函數私有
	{}
	PageCache(const PageCache&) = delete; //防拷貝

	static PageCache _sInst;
};

當程序運行起來后我們就立馬創(chuàng)建該對象即可。

PageCache PageCache::_sInst;

pagecache中獲取Span

獲取一個非空的span

thread cache向central cache申請對象時,central cache需要先從對應的哈希桶中獲取到一個非空的span,然后從這個非空的span中取出若干對象返回給thread cache。那central cache到底是如何從對應的哈希桶中,獲取到一個非空的span的呢?

首先當然是先遍歷central cache對應哈希桶當中的雙鏈表,如果該雙鏈表中有非空的span,那么直接將該span進行返回即可。為了方便遍歷這個雙鏈表,我們可以模擬迭代器的方式,給SpanList類提供Begin和End成員函數,分別用于獲取雙鏈表中的第一個span和最后一個span的下一個位置,也就是頭結點。

//帶頭雙向循環(huán)鏈表
class SpanList
{
public:
	Span* Begin()
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}
private:
	Span* _head;
public:
	std::mutex _mtx; //桶鎖
};

但如果遍歷雙鏈表后發(fā)現雙鏈表中沒有span,或該雙鏈表中的span都為空,那么此時central cache就需要向page cache申請內存塊了。

那具體是向page cache申請多大的內存塊呢?我們可以根據具體所需對象的大小來決定,就像之前我們根據對象的大小計算出,thread cache一次向central cache申請對象的個數上限,現在我們是根據對象的大小計算出,central cache一次應該向page cache申請幾頁的內存塊。

我們可以先根據對象的大小計算出,thread cache一次向central cache申請對象的個數上限,然后將這個上限值乘以單個對象的大小,就算出了具體需要多少字節(jié),最后再將這個算出來的字節(jié)數轉換為頁數,如果轉換后不夠一頁,那么我們就申請一頁,否則轉換出來是幾頁就申請幾頁。也就是說,central cache向page cache申請內存時,要求申請到的內存盡量能夠滿足thread cache向central cache申請時的上限。

//管理對齊和映射等關系
class SizeClass
{
public:
	//central cache一次向page cache獲取多少頁
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size); //計算出thread cache一次向central cache申請對象的個數上限
		size_t nPage = num*size; //num個size大小的對象所需的字節(jié)數

		nPage >>= PAGE_SHIFT; //將字節(jié)數轉換為頁數
		if (nPage == 0) //至少給一頁
			nPage = 1;

		return nPage;
	}
};

代碼中的PAGE_SHIFT代表頁大小轉換偏移,我們這里以頁的大小為8K為例,PAGE_SHIFT的值就是13。

//頁大小轉換偏移,即一頁定義為2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;

需要注意的是,當central cache申請到若干頁的span后,還需要將這個span切成一個個對應大小的對象掛到該span的自由鏈表當中。

如何找到一個span所管理的內存塊呢?首先需要計算出該span的起始地址,我們可以用這個span的起始頁號乘以一頁的大小即可得到這個span的起始地址,然后用這個span的頁數乘以一頁的大小就可以得到這個span所管理的內存塊的大小,用起始地址加上內存塊的大小即可得到這塊內存塊的結束位置。

明確了這塊內存的起始和結束位置后,我們就可以進行切分了。根據所需對象的大小,每次從大塊內存切出一塊固定大小的內存塊尾插到span的自由鏈表中即可。

為什么是尾插呢?因為我們如果是將切好的對象尾插到自由鏈表,這些對象看起來是按照鏈式結構鏈接起來的,而實際它們在物理上是連續(xù)的,這時當我們把這些連續(xù)內存分配給某個線程使用時,可以提高該線程的CPU緩存利用率。

//獲取一個非空的span
Span* CentralCache::GetOneSpan(SpanList& spanList, size_t size)
{
	//1、先在spanList中尋找非空的span
	Span* it = spanList.Begin();
	while (it != spanList.End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}

	//2、spanList中沒有非空的span,只能向page cache申請
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	//計算span的大塊內存的起始地址和大塊內存的大?。ㄗ止?jié)數)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	size_t bytes = span->_n << PAGE_SHIFT;

	//把大塊內存切成size大小的對象鏈接起來
	char* end = start + bytes;
	//先切一塊下來去做尾,方便尾插
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	//尾插
	while (start < end)
	{
		NextObj(tail) = start;
		tail = NextObj(tail);
		start += size;
	}
	NextObj(tail) = nullptr; //尾的指向置空
	
	//將切好的span頭插到spanList
	spanList.PushFront(span);

	return span;
}

需要注意的是,當我們把span切好后,需要將這個切好的span掛到central cache的對應哈希桶中。因此SpanList類還需要提供一個接口,用于將一個span插入到該雙鏈表中。這里我們選擇的是頭插,這樣當central cache下一次從該雙鏈表中獲取非空span時,一來就能找到。

由于SpanList類之前實現了Insert和Begin函數,這里實現雙鏈表頭插就非常簡單,直接在雙鏈表的Begin位置進行Insert即可。

//帶頭雙向循環(huán)鏈表
class SpanList
{
public:
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}
private:
	Span* _head;
public:
	std::mutex _mtx; //桶鎖
};

獲取一個k頁的span

當我們調用上述的GetOneSpan從central cache的某個哈希桶獲取一個非空的span時,如果遍歷哈希桶中的雙鏈表后發(fā)現雙鏈表中沒有span,或該雙鏈表中的span都為空,那么此時central cache就需要向page cache申請若干頁的span了,下面我們就來說說如何從page cache獲取一個k頁的span。

因為page cache是直接按照頁數進行映射的,因此我們要從page cache獲取一個k頁的span,就應該直接先去找page cache的第k號桶,如果第k號桶中有span,那我們直接頭刪一個span返回給central cache就行了。所以我們這里需要再給SpanList類添加對應的Empty和PopFront函數。

//帶頭雙向循環(huán)鏈表
class SpanList
{
public:
	bool Empty()
	{
		return _head == _head->_next;
	}
	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}
private:
	Span* _head;
public:
	std::mutex _mtx; //桶鎖
};

如果page cache的第k號桶中沒有span,我們就應該繼續(xù)找后面的桶,只要后面任意一個桶中有一個n頁span,我們就可以將其切分成一個k頁的span和一個n-k頁的span,然后將切出來k頁的span返回給central cache,再將n-k頁的span掛到page cache的第n-k號桶即可。

但如果后面的桶中也都沒有span,此時我們就需要向堆申請一個128頁的span了,在向堆申請內存時,直接調用我們封裝的SystemAlloc函數即可。

需要注意的是,向堆申請內存后得到的是這塊內存的起始地址,此時我們需要將該地址轉換為頁號。由于我們向堆申請內存時都是按頁進行申請的,因此我們直接將該地址除以一頁的大小即可得到對應的頁號。

//獲取一個k頁的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//先檢查第k個桶里面有沒有span
	if (!_spanLists[k].Empty())
	{
		return _spanLists[k].PopFront();
	}
	//檢查一下后面的桶里面有沒有span,如果有可以將其進行切分
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的頭部切k頁下來
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//將剩下的掛到對應映射的位置
			_spanLists[nSpan->_n].PushFront(nSpan);
			return kSpan;
		}
	}
	//走到這里說明后面沒有大頁的span了,這時就向堆申請一個128頁的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	//盡量避免代碼重復,遞歸調用自己
	return NewSpan(k);
}

這里說明一下,當我們向堆申請到128頁的span后,需要將其切分成k頁的span和128-k頁的span,但是為了盡量避免出現重復的代碼,我們最好不要再編寫對應的切分代碼。我們可以先將申請到的128頁的span掛到page cache對應的哈希桶中,然后再遞歸調用該函數就行了,此時在往后找span時就一定會在第128號桶中找到該span,然后進行切分。

這里其實有一個問題:當central cache向page cache申請內存時,central cache對應的哈希桶是處于加鎖的狀態(tài)的,那在訪問page cache之前我們應不應該把central cache對應的桶鎖解掉呢?

這里建議在訪問page cache前,先把central cache對應的桶鎖解掉。雖然此時central cache的這個桶當中是沒有內存供其他thread cache申請的,但thread cache除了申請內存還會釋放內存,如果在訪問page cache前將central cache對應的桶鎖解掉,那么此時當其他thread cache想要歸還內存到central cache的這個桶時就不會被阻塞。

因此在調用NewSpan函數之前,我們需要先將central cache對應的桶鎖解掉,然后再將page cache的大鎖加上,當申請到k頁的span后,我們需要將page cache的大鎖解掉,但此時我們不需要立刻獲取到central cache中對應的桶鎖。因為central cache拿到k頁的span后還會對其進行切分操作,因此我們可以在span切好后需要將其掛到central cache對應的桶上時,再獲取對應的桶鎖。

這里為了讓代碼清晰一點,只寫出了加鎖和解鎖的邏輯,我們只需要將這些邏輯添加到之前實現的GetOneSpan函數的對應位置即可。

spanList._mtx.unlock(); //解桶鎖
PageCache::GetInstance()->_pageMtx.lock(); //加大鎖

//從page cache申請k頁的span

PageCache::GetInstance()->_pageMtx.unlock(); //解大鎖

//進行span的切分...

spanList._mtx.lock(); //加桶鎖

//將span掛到central cache對應的哈希桶

申請內存過程聯(lián)調

ConcurrentAlloc函數

在將thread cache、central cache以及page cache的申請流程寫通了之后,我們就可以向外提供一個ConcurrentAlloc函數,用于申請內存塊。每個線程第一次調用該函數時會通過TLS獲取到自己專屬的thread cache對象,然后每個線程就可以通過自己對應的thread cache申請對象了。

static void* ConcurrentAlloc(size_t size)
{
	//通過TLS,每個線程無鎖的獲取自己專屬的ThreadCache對象
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}
	return pTLSThreadCache->Allocate(size);
}

這里說一下編譯時會出現的問題,在C++的algorithm頭文件中有一個min函數,這是一個函數模板,而在Windows.h頭文件中也有一個min,這是一個宏。由于調用函數模板時需要進行參數類型的推演,因此當我們調用min函數時,編譯器會優(yōu)先匹配Windows.h當中以宏的形式實現的min,此時當我們以std::min的形式調用min函數時就會產生報錯,這就是沒有用命名空間進行封裝的壞處,這時我們只能選擇將std::去掉,讓編譯器調用Windows.h當中的min。

申請內存過程聯(lián)調測試一

由于在多線程場景下調試觀察起來非常麻煩,這里就先不考慮多線程場景,看看在單線程場景下代碼的執(zhí)行邏輯是否符合我們的預期,其次,我們這里就只簡單觀察在一個桶當中的內存申請就行了。

下面該線程進行了三次內存申請,這三次內存申請的字節(jié)數最終都對齊到了8,此時當線程申請內存時就只會訪問到thread cache的第0號桶。

void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);

當線程第一次申請內存時,該線程需要通過TLS獲取到自己專屬的thread cache對象,然后通過這個thread cache對象進行內存申請。

在這里插入圖片描述

在申請內存時通過計算索引到了thread cache的第0號桶,但此時thread cache的第0號桶中是沒有對象的,因此thread cache需要向central cache申請內存塊。

在這里插入圖片描述

在向central cache申請內存塊前,首先通過NumMoveSize函數計算得出,thread cache一次最多可向central cache申請8字節(jié)大小對象的個數是512,但由于我們采用的是慢開始算法,因此還需要將上限值與對應自由鏈表的_maxSize的值進行比較,而此時對應自由鏈表_maxSize的值是1,所以最終得出本次thread cache向central cache申請8字節(jié)對象的個數是1個。

并且在此之后會將該自由鏈表中_maxSize的值進行自增,下一次thread cache再向central cache申請8字節(jié)對象時最終申請對象的個數就會是2個了。

在這里插入圖片描述

在thread cache向central cache申請對象之前,需要先將central cache的0號桶的鎖加上,然后再從該桶獲取一個非空的span。

在這里插入圖片描述

在central cache的第0號桶獲取非空span時,先遍歷對應的span雙鏈表,看看有沒有非空的span,但此時肯定是沒有的,因此在這個過程中我們無法找到一個非空的span。

在這里插入圖片描述

那么此時central cache就需要向page cache申請內存了,但在此之前需要先把central cache第0號桶的鎖解掉,然后再將page cache的大鎖給加上,之后才能向page cache申請內存。

在這里插入圖片描述

在向page cache申請內存時,由于central cache一次給thread cache8字節(jié)對象的上限是512,對應就需要4096字節(jié),所需字節(jié)數不足一頁就按一頁算,所以這里central cache就需要向page cache申請一頁的內存塊。

在這里插入圖片描述

但此時page cache的第1個桶以及之后的桶當中都是沒有span的,因此page cache需要直接向堆申請一個128頁的span。

在這里插入圖片描述

這里通過監(jiān)視窗口可以看到,用于管理申請到的128頁內存的span信息。

在這里插入圖片描述

我們可以順便驗證一下,按頁向堆申請的內存塊的起始地址和頁號之間是可以相互轉換的。

在這里插入圖片描述

現在將申請到的128頁的span插入到page cache的第128號桶當中,然后再調用一次NewSpan,在這次調用的時候,雖然在1號桶當中沒有span,但是在往后找的過程中就一定會在第128號桶找到一個span。

在這里插入圖片描述

此時我們就可以把這個128頁的span拿出來,切分成1頁的span和127頁的span,將1頁的span返回給central cache,而把127頁的span掛到page cache的第127號桶即可。

在這里插入圖片描述

從page cache返回后,就可以把page cache的大鎖解掉了,但緊接著還要將獲取到的1頁的span進行切分,因此這里沒有立刻重新加上central cache對應的桶鎖。

在這里插入圖片描述

在進行切分的時候,先通過該span的起始頁號得到該span的起始地址,然后通過該span的頁數得到該span所管理內存塊的總的字節(jié)數。

在這里插入圖片描述

在確定內存塊的開始和結束后,就可以將其切分成一個個8字節(jié)大小的對象掛到該span的自由鏈表中了。在調試過程中通過內存監(jiān)視窗口可以看到,切分出來的每個8字節(jié)大小的對象的前四個字節(jié)存儲的都是下一個8字節(jié)對象的起始地址。

在這里插入圖片描述

當切分結束后再獲取central cache第0號桶的桶鎖,然后將這個切好的span插入到central cache的第0號桶中,最后再將這個非空的span返回,此時就獲取到了一個非空的span。

在這里插入圖片描述

由于thread cache只向central cache申請了一個對象,因此拿到這個非空的span后,直接從這個span里面取出一個對象即可,此時該span的_useCount也由0變成了1。

在這里插入圖片描述

由于此時thread cache實際只向central cache申請到了一個對象,因此直接將這個對象返回給線程即可。

在這里插入圖片描述

當線程第二次申請內存塊時就不會再創(chuàng)建thread cache了,因為第一次申請時就已經創(chuàng)建好了,此時該線程直接獲取到對應的thread cache進行內存塊申請即可。

在這里插入圖片描述

當該線程第二次申請8字節(jié)大小的對象時,此時thread cache的0號桶中還是沒有對象的,因為第一次thread cache只向central cache申請了一個8字節(jié)對象,因此這次申請時還需要再向central cache申請對象。

在這里插入圖片描述

這時thread cache向central cache申請對象時,thread cache第0號桶中自由鏈表的_maxSize已經慢增長到2了,所以這次在向central cache申請對象時就會申請2個。如果下一次thread cache再向central cache申請8字節(jié)大小的對象,那么central cache會一次性給thread cache3個,這就是所謂的慢增長。

在這里插入圖片描述

但由于第一次central cache向page cache申請了一頁的內存塊,并將其切成了1024個8字節(jié)大小的對象,因此這次thread cache向central cache申請2兩個8字節(jié)的對象時,central cache的第0號桶當中是有對象的,直接返回兩個給thread cache即可,而不用再向page cache申請內存了。

但線程實際申請的只是一個8字節(jié)對象,因此thread cache除了將一個對象返回之外,還需要將剩下的一個對象掛到thread cache的第0號桶當中。

在這里插入圖片描述

這樣一來,當線程第三次申請1字節(jié)的內存時,由于1字節(jié)對齊后也是8字節(jié),此時thread cache也就不需要再向central cache申請內存塊了,直接將第0號桶當中之前剩下的一個8字節(jié)對象返回即可。

在這里插入圖片描述

申請內存過程聯(lián)調測試二

為了進一步測試代碼的正確性,我們可以做這樣一個測試:讓線程申請1024次8字節(jié)的對象,然后通過調試觀察在第1025次申請時,central cache是否會再向page cache申請內存塊。

for (size_t i = 0; i < 1024; i++)
{
	void* p1 = ConcurrentAlloc(6);
}
void* p2 = ConcurrentAlloc(6);

因為central cache第一次就是向page cache申請的一頁內存,這一頁內存被切成了1024個8字節(jié)大小的對象,當這1024個對象全部被申請之后,再申請8字節(jié)大小的對象時central cache當中就沒有對象了,此時就應該向page cache申請內存塊。

通過調試我們可以看到,第1025次申請8字節(jié)大小的對象時,central cache第0號桶中的這個span的_useCount已經增加到了1024,也就是說這1024個對象都已經被線程申請了,此時central cache就需要再向page cache申請一頁的span來進行切分了。

在這里插入圖片描述

而這次central cache在向page cache申請一頁的內存時,page cache就是將127頁span切分成了1頁的span和126頁的span了,然后central cache拿到這1頁的span后,又會將其切分成1024塊8字節(jié)大小的內存塊以供thread cache申請。

在這里插入圖片描述

threadcache回收內存

當某個線程申請的對象不用了,可以將其釋放給thread cache,然后thread cache將該對象插入到對應哈希桶的自由鏈表當中即可。

但是隨著線程不斷的釋放,對應自由鏈表的長度也會越來越長,這些內存堆積在一個thread cache中就是一種浪費,我們應該將這些內存還給central cache,這樣一來,這些內存對其他線程來說也是可申請的,因此當thread cache某個桶當中的自由鏈表太長時我們可以進行一些處理。

如果thread cache某個桶當中自由鏈表的長度超過它一次批量向central cache申請的對象個數,那么此時我們就要把該自由鏈表當中的這些對象還給central cache。

//釋放內存對象
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);

	//找出對應的自由鏈表桶將對象插入
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(ptr);

	//當自由鏈表長度大于一次批量申請的對象個數時就開始還一段list給central cache
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

當自由鏈表的長度大于一次批量申請的對象時,我們具體的做法就是,從該自由鏈表中取出一次批量個數的對象,然后將取出的這些對象還給central cache中對應的span即可。

//釋放對象導致鏈表過長,回收內存到中心緩存
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	//從list中取出一次批量個數的對象
	list.PopRange(start, end, list.MaxSize());
	
	//將取出的對象還給central cache中對應的span
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

從上述代碼可以看出,FreeList類需要支持用Size函數獲取自由鏈表中對象的個數,還需要支持用PopRange函數從自由鏈表中取出指定個數的對象。因此我們需要給FreeList類增加一個對應的PopRange函數,然后再增加一個_size成員變量,該成員變量用于記錄當前自由鏈表中對象的個數,當我們向自由鏈表插入或刪除對象時,都應該更新_size的值。

//管理切分好的小對象的自由鏈表
class FreeList
{
public:
	//將釋放的對象頭插到自由鏈表
	void Push(void* obj)
	{
		assert(obj);

		//頭插
		NextObj(obj) = _freeList;
		_freeList = obj;
		_size++;
	}
	//從自由鏈表頭部獲取一個對象
	void* Pop()
	{
		assert(_freeList);

		//頭刪
		void* obj = _freeList;
		_freeList = NextObj(_freeList);
		_size--;

		return obj;
	}
	//插入一段范圍的對象到自由鏈表
	void PushRange(void* start, void* end, size_t n)
	{
		assert(start);
		assert(end);

		//頭插
		NextObj(end) = _freeList;
		_freeList = start;
		_size += n;
	}
	//從自由鏈表獲取一段范圍的對象
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n <= _size);

		//頭刪
		start = _freeList;
		end = start;
		for (size_t i = 0; i < n - 1;i++)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end); //自由鏈表指向end的下一個對象
		NextObj(end) = nullptr; //取出的一段鏈表的表尾置空
		_size -= n;
	}
	bool Empty()
	{
		return _freeList == nullptr;
	}
	size_t& MaxSize()
	{
		return _maxSize;
	}
	size_t Size()
	{
		return _size;
	}
private:
	void* _freeList = nullptr; //自由鏈表
	size_t _maxSize = 1;
	size_t _size = 0;
};

而對于FreeList類當中的PushRange成員函數,我們最好也像PopRange一樣給它增加一個參數,表示插入對象的個數,不然我們這時還需要通過遍歷統(tǒng)計插入對象的個數。

因此之前在調用PushRange的地方就需要修改一下,而我們實際就在一個地方調用過PushRange函數,并且此時插入對象的個數也是很容易知道的。當時thread cache從central cache獲取了actualNum個對象,將其中的一個返回給了申請對象的線程,剩下的actualNum-1個掛到了thread cache對應的桶當中,所以這里插入對象的個數就是actualNum-1。

_freeLists[index].PushRange(NextObj(start), end, actualNum - 1);

說明一下:
當thread cache的某個自由鏈表過長時,我們實際就是把這個自由鏈表當中全部的對象都還給central cache了,但這里在設計PopRange接口時還是設計的是取出指定個數的對象,因為在某些情況下當自由鏈表過長時,我們可能并不一定想把鏈表中全部的對象都取出來還給central cache,這樣設計就是為了增加代碼的可修改性。

其次,當我們判斷thread cache是否應該還對象給central cache時,還可以綜合考慮每個thread cache整體的大小。比如當某個thread cache的總占用大小超過一定閾值時,我們就將該thread cache當中的對象還一些給central cache,這樣就盡量避免了某個線程的thread cache占用太多的內存。對于這一點,在tcmalloc當中就是考慮到了的。

centralcache回收內存

當thread cache中某個自由鏈表太長時,會將自由鏈表當中的這些對象還給central cache中的span。

但是需要注意的是,還給central cache的這些對象不一定都是屬于同一個span的。central cache中的每個哈希桶當中可能都不止一個span,因此當我們計算出還回來的對象應該還給central cache的哪一個桶后,還需要知道這些對象到底應該還給這個桶當中的哪一個span。

如何根據對象的地址得到對象所在的頁號?

首先我們必須理解的是,某個頁當中的所有地址除以頁的大小都等該頁的頁號。比如我們這里假設一頁的大小是100,那么地址0~99都屬于第0頁,它們除以100都等于0,而地址100~199都屬于第1頁,它們除以100都等于1。

如何找到一個對象對應的span?

雖然我們現在可以通過對象的地址得到其所在的頁號,但是我們還是不能知道這個對象到底屬于哪一個span。因為一個span管理的可能是多個頁。

為了解決這個問題,我們可以建立頁號和span之間的映射。由于這個映射關系在page cache進行span的合并時也需要用到,因此我們直接將其存放到page cache里面。這時我們就需要在PageCache類當中添加一個映射關系了,這里可以用C++當中的unordered_map進行實現,并且添加一個函數接口,用于讓central cache獲取這里的映射關系。(下面代碼中只展示了PageCache類當中新增的成員)

//單例模式class PageCache{<!--{C}%3C!%2D%2D%20%2D%2D%3E-->public://獲取從對象到span的映射Span* MapObjectToSpan(void* obj);private:std::unordered_map<PAGE_ID, Span*> _idSpanMap;};

每當page cache分配span給central cache時,都需要記錄一下頁號和span之間的映射關系。此后當thread cache還對象給central cache時,才知道應該具體還給哪一個span。

因此當central cache在調用NewSpan接口向page cache申請k頁的span時,page cache在返回這個k頁的span給central cache之前,應該建立這k個頁號與該span之間的映射關系。

//獲取一個k頁的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//先檢查第k個桶里面有沒有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}
	//檢查一下后面的桶里面有沒有span,如果有可以將其進行切分
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的頭部切k頁下來
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//將剩下的掛到對應映射的位置
			_spanLists[nSpan->_n].PushFront(nSpan);

			//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}
	//走到這里說明后面沒有大頁的span了,這時就向堆申請一個128頁的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	//盡量避免代碼重復,遞歸調用自己
	return NewSpan(k);
}

此時我們就可以通過對象的地址找到該對象對應的span了,直接將該對象的地址除以頁的大小得到頁號,然后在unordered_map當中找到其對應的span即可。

//獲取從對象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT; //頁號
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

注意一下,當我們要通過某個頁號查找其對應的span時,該頁號與其span之間的映射一定是建立過的,如果此時我們沒有在unordered_map當中找到,則說明我們之前的代碼邏輯有問題,因此當沒有找到對應的span時可以直接用斷言結束程序,以表明程序邏輯出錯。

central cache回收內存

這時當thread cache還對象給central cache時,就可以依次遍歷這些對象,將這些對象插入到其對應span的自由鏈表當中,并且及時更新該span的_usseCount計數即可。

在thread cache還對象給central cache的過程中,如果central cache中某個span的_useCount減到0時,說明這個span分配出去的對象全部都還回來了,那么此時就可以將這個span再進一步還給page cache。

//將一定數量的對象還給對應的span
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock(); //加鎖
	while (start)
	{
		void* next = NextObj(start); //記錄下一個
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//將對象頭插到span的自由鏈表
		NextObj(start) = span->_freeList;
		span->_freeList = start;

		span->_useCount--; //更新被分配給thread cache的計數
		if (span->_useCount == 0) //說明這個span分配出去的對象全部都回來了
		{
			//此時這個span就可以再回收給page cache,page cache可以再嘗試去做前后頁的合并
			_spanLists[index].Erase(span);
			span->_freeList = nullptr; //自由鏈表置空
			span->_next = nullptr;
			span->_prev = nullptr;

			//釋放span給page cache時,使用page cache的鎖就可以了,這時把桶鎖解掉
			_spanLists[index]._mtx.unlock(); //解桶鎖
			PageCache::GetInstance()->_pageMtx.lock(); //加大鎖
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock(); //解大鎖
			_spanLists[index]._mtx.lock(); //加桶鎖
		}

		start = next;
	}

	_spanLists[index]._mtx.unlock(); //解鎖
}

需要注意,如果要把某個span還給page cache,我們需要先將這個span從central cache對應的雙鏈表中移除,然后再將該span的自由鏈表置空,因為page cache中的span是不需要切分成一個個的小對象的,以及該span的前后指針也都應該置空,因為之后要將其插入到page cache對應的雙鏈表中。但span當中記錄的起始頁號以及它管理的頁數是不能清除的,否則對應內存塊就找不到了。

并且在central cache還span給page cache時也存在鎖的問題,此時需要先將central cache中對應的桶鎖解掉,然后再加上page cache的大鎖之后才能進入page cache進行相關操作,當處理完畢回到central cache時,除了將page cache的大鎖解掉,還需要立刻獲得central cache對應的桶鎖,然后將還未還完對象繼續(xù)還給central cache中對應的span。

pagecache回收內存

如果central cache中有某個span的_useCount減到0了,那么central cache就需要將這個span還給page cache了。

這個過程看似是非常簡單的,page cache只需將還回來的span掛到對應的哈希桶上就行了。但實際為了緩解內存碎片的問題,page cache還需要嘗試將還回來的span與其他空閑的span進行合并。

page cache進行前后頁的合并

合并的過程可以分為向前合并和向后合并。如果還回來的span的起始頁號是num,該span所管理的頁數是n。那么在向前合并時,就需要判斷第num-1頁對應span是否空閑,如果空閑則可以將其進行合并,并且合并后還需要繼續(xù)向前嘗試進行合并,直到不能進行合并為止。而在向后合并時,就需要判斷第num+n頁對應的span是否空閑,如果空閑則可以將其進行合并,并且合并后還需要繼續(xù)向后嘗試進行合并,直到不能進行合并為止。

因此page cache在合并span時,是需要通過頁號獲取到對應的span的,這就是我們要把頁號與span之間的映射關系存儲到page cache的原因。

但需要注意的是,當我們通過頁號找到其對應的span時,這個span此時可能掛在page cache,也可能掛在central cache。而在合并時我們只能合并掛在page cache的span,因為掛在central cache的span當中的對象正在被其他線程使用。

可是我們不能通過span結構當中的_useCount成員,來判斷某個span到底是在central cache還是在page cache。因為當central cache剛向page cache申請到一個span時,這個span的_useCount就是等于0的,這時可能當我們正在對該span進行切分的時候,page cache就把這個span拿去進行合并了,這顯然是不合理的。

鑒于此,我們可以在span結構中再增加一個_isUse成員,用于標記這個span是否正在被使用,而當一個span結構被創(chuàng)建時我們默認該span是沒有被使用的。

//管理以頁為單位的大塊內存
struct Span
{
	PAGE_ID _pageId = 0;        //大塊內存起始頁的頁號
	size_t _n = 0;              //頁的數量

	Span* _next = nullptr;      //雙鏈表結構
	Span* _prev = nullptr;

	size_t _useCount = 0;       //切好的小塊內存,被分配給thread cache的計數
	void* _freeList = nullptr;  //切好的小塊內存的自由鏈表

	bool _isUse = false;        //是否在被使用
};

因此當central cache向page cache申請到一個span時,需要立即將該span的_isUse改為true。

span->_isUse = true;

而當central cache將某個span還給page cache時,也就需要將該span的_isUse改成false。

span->_isUse = false;

由于在合并page cache當中的span時,需要通過頁號找到其對應的span,而一個span是在被分配給central cache時,才建立的各個頁號與span之間的映射關系,因此page cache當中的span也需要建立頁號與span之間的映射關系。

與central cache中的span不同的是,在page cache中,只需建立一個span的首尾頁號與該span之間的映射關系。因為當一個span在嘗試進行合并時,如果是往前合并,那么只需要通過一個span的尾頁找到這個span,如果是向后合并,那么只需要通過一個span的首頁找到這個span。也就是說,在進行合并時我們只需要用到span與其首尾頁之間的映射關系就夠了。

因此當我們申請k頁的span時,如果是將n頁的span切成了一個k頁的span和一個n-k頁的span,我們除了需要建立k頁span中每個頁與該span之間的映射關系之外,還需要建立剩下的n-k頁的span與其首尾頁之間的映射關系。

//獲取一個k頁的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//先檢查第k個桶里面有沒有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}
	//檢查一下后面的桶里面有沒有span,如果有可以將其進行切分
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的頭部切k頁下來
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//將剩下的掛到對應映射的位置
			_spanLists[nSpan->_n].PushFront(nSpan);
			//存儲nSpan的首尾頁號與nSpan之間的映射,方便page cache合并span時進行前后頁的查找
			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;

			//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}
	//走到這里說明后面沒有大頁的span了,這時就向堆申請一個128頁的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	//盡量避免代碼重復,遞歸調用自己
	return NewSpan(k);
}

此時page cache當中的span就都與其首尾頁之間建立了映射關系,現在我們就可以進行span的合并了,其合并邏輯如下:

//釋放空閑的span回到PageCache,并合并相鄰的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//對span的前后頁,嘗試進行合并,緩解內存碎片問題
	//1、向前合并
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		//前面的頁號沒有(還未向系統(tǒng)申請),停止向前合并
		if (ret == _idSpanMap.end())
		{
			break;
		}
		//前面的頁號對應的span正在被使用,停止向前合并
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}
		//合并出超過128頁的span無法進行管理,停止向前合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//進行向前合并
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		//將prevSpan從對應的雙鏈表中移除
		_spanLists[prevSpan->_n].Erase(prevSpan);

		delete prevSpan;
	}
	//2、向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		//后面的頁號沒有(還未向系統(tǒng)申請),停止向后合并
		if (ret == _idSpanMap.end())
		{
			break;
		}
		//后面的頁號對應的span正在被使用,停止向后合并
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}
		//合并出超過128頁的span無法進行管理,停止向后合并
		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//進行向后合并
		span->_n += nextSpan->_n;

		//將nextSpan從對應的雙鏈表中移除
		_spanLists[nextSpan->_n].Erase(nextSpan);

		delete nextSpan;
	}
	//將合并后的span掛到對應的雙鏈表當中
	_spanLists[span->_n].PushFront(span);
	//建立該span與其首尾頁的映射
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
	//將該span設置為未被使用的狀態(tài)
	span->_isUse = false;
}

需要注意的是,在向前或向后進行合并的過程中:

  • 如果沒有通過頁號獲取到其對應的span,說明對應到該頁的內存塊還未申請,此時需要停止合并。
  • 如果通過頁號獲取到了其對應的span,但該span處于被使用的狀態(tài),那我們也必須停止合并。
  • 如果合并后大于128頁則不能進行本次合并,因為page cache無法對大于128頁的span進行管理。

在合并span時,由于這個span是在page cache的某個哈希桶的雙鏈表當中的,因此在合并后需要將其從對應的雙鏈表中移除,然后再將這個被合并了的span結構進行delete。

除此之外,在合并結束后,除了將合并后的span掛到page cache對應哈希桶的雙鏈表當中,還需要建立該span與其首位頁之間的映射關系,便于此后合并出更大的span。

釋放內存過程聯(lián)調

ConcurrentFree函數

至此我們將thread cache、central cache以及page cache的釋放流程也都寫完了,此時我們就可以向外提供一個ConcurrentFree函數,用于釋放內存塊,釋放內存塊時每個線程通過自己的thread cache對象,調用thread cache中釋放內存對象的接口即可。

static void ConcurrentFree(void* ptr, size_t size/*暫時*/)
{
	assert(pTLSThreadCache);

	pTLSThreadCache->Deallocate(ptr, size);
}

釋放內存過程聯(lián)調測試

之前我們在測試申請流程時,讓單個線程進行了三次內存申請,現在我們再將這三個對象再進行釋放,看看這其中的釋放流程是如何進行的。

void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);

ConcurrentFree(p1, 6);
ConcurrentFree(p2, 8);
ConcurrentFree(p3, 1);

首先,這三次申請和釋放的對象大小進行對齊后都是8字節(jié),因此對應操作的就是thread cache和central cache的第0號桶,以及page cache的第1號桶。

由于第三次對象申請時,剛好將thread cache第0號桶當中僅剩的一個對象拿走了,因此在三次對象申請后thread cache的第0號桶當中是沒有對象的。

通過監(jiān)視窗口可以看到,此時thread cache第0號桶中自由鏈表的_maxSize已經慢增長到了3,而當我們釋放完第一個對象后,該自由鏈表當中對象的個數只有一個,因此不會將該自由鏈表當中的對象進一步還給central cache。

在這里插入圖片描述

當第二個對象釋放給thread cache的第0號桶后,該桶對應自由鏈表當中對象的個數變成了2,也是不會進行ListTooLong操作的。

在這里插入圖片描述

直到第三個對象釋放給thread cache的第0號桶時,此時該自由鏈表的_size的值變?yōu)?,與_maxSize的值相等,現在thread cache就需要將對象給central cache了。

在這里插入圖片描述

thread cache先是將第0號桶當中的對象彈出MaxSize個,在這里實際上就是全部彈出,此時該自由鏈表_size的值變?yōu)?,然后繼續(xù)調用central cache當中的ReleaseListToSpans函數,將這三個對象還給central cache當中對應的span。

在這里插入圖片描述

在進入central cache的第0號桶還對象之前,先把第0號桶對應的桶鎖加上,然后通過查page cache中的映射表找到其對應的span,最后將這個對象頭插到該span的自由鏈表中,并將該span的_useCount進行--。當第一個對象還給其對應的span時,可以看到該span的_useCount減到了2。

在這里插入圖片描述

而由于我們只進行了三次對象申請,并且這些對象大小對齊后大小都是8字節(jié),因此我們申請的這三個對象實際都是同一個span切分出來的。當我們將這三個對象都還給這個span時,該span的_useCount就減為了0。

在這里插入圖片描述

現在central cache就需要將這個span進一步還給page cache,而在將該span交給page cache之前,會將該span的自由鏈表以及前后指針都置空。并且在進入page cache之前會先將central cache第0號桶的桶鎖解掉,然后再加上page cache的大鎖,之后才能進入page cache進行相關操作。

在這里插入圖片描述

由于這個一頁的span是從128頁的span的頭部切下來的,在向前合并時由于前面的頁還未向系統(tǒng)申請,因此在查映射關系時是無法找到的,此時直接停止了向前合并。

(說明一下:由于下面是重新另外進行的一次調試,因此監(jiān)視窗口顯示的span的起始頁號與之前的不同,實際應該與上面一致)

在這里插入圖片描述

而在向后合并時,由于page cache沒有將該頁后面的頁分配給central cache,因此在向后合并時肯定能夠找到一個127頁的span進行合并。合并后就變成了一個128頁的span,這時我們將原來127頁的span從第127號桶刪除,然后還需要將該127頁的span結構進行delete,因為它管理的127頁已經與1頁的span進行合并了,不再需要它來管理了。

在這里插入圖片描述

緊接著將這個128頁的span插入到第128號桶,然后建立該span與其首尾頁的映射,便于下次被用于合并,最后再將該span的狀態(tài)設置為未被使用的狀態(tài)即可。

在這里插入圖片描述

當從page cache回來后,除了將page cache的大鎖解掉,還需要立刻加上central cache中對應的桶鎖,然后繼續(xù)將對象還給central cache中的span,但此時實際上是還完了,因此再將central cache的桶鎖解掉就行了。

在這里插入圖片描述

至此我們便完成了這三個對象的申請和釋放流程。

大于256KB的大塊內存申請問題

申請過程

之前說到,每個線程的thread cache是用于申請小于等于256KB的內存的,而對于大于256KB的內存,我們可以考慮直接向page cache申請,但page cache中最大的頁也就只有128頁,因此如果是大于128頁的內存申請,就只能直接向堆申請了。

申請內存的大小申請方式
x ≤ 256 K B ( 32 頁 ) x \leq 256KB(32頁) x≤256KB(32頁)向thread cache申請
32 頁 < x ≤ 128 頁 32 頁< x \leq 128頁 32頁<x≤128頁向page cache申請
x ≥ 128 頁 x \geq 128頁 x≥128頁向堆申請
  當申請的內存大于256KB時,雖然不是從thread cache進行獲取,但在分配內存時也是需要進行向上對齊的,對于大于256KB的內存我們可以直接按頁進行對齊。 

而我們之前實現RoundUp函數時,對傳入字節(jié)數大于256KB的情況直接做了斷言處理,因此這里需要對RoundUp函數稍作修改。

//獲取向上對齊后的字節(jié)數
static inline size_t RoundUp(size_t bytes)
{
	if (bytes <= 128)
	{
		return _RoundUp(bytes, 8);
	}
	else if (bytes <= 1024)
	{
		return _RoundUp(bytes, 16);
	}
	else if (bytes <= 8 * 1024)
	{
		return _RoundUp(bytes, 128);
	}
	else if (bytes <= 64 * 1024)
	{
		return _RoundUp(bytes, 1024);
	}
	else if (bytes <= 256 * 1024)
	{
		return _RoundUp(bytes, 8 * 1024);
	}
	else
	{
		//大于256KB的按頁對齊
		return _RoundUp(bytes, 1 << PAGE_SHIFT);
	}
}

現在對于之前的申請邏輯就需要進行修改了,當申請對象的大小大于256KB時,就不用向thread cache申請了,這時先計算出按頁對齊后實際需要申請的頁數,然后通過調用NewSpan申請指定頁數的span即可。

static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES) //大于256KB的內存申請
	{
		//計算出對齊后需要申請的頁數
		size_t alignSize = SizeClass::RoundUp(size);
		size_t kPage = alignSize >> PAGE_SHIFT;

		//向page cache申請kPage頁的span
		PageCache::GetInstance()->_pageMtx.lock();
		Span* span = PageCache::GetInstance()->NewSpan(kPage);
		PageCache::GetInstance()->_pageMtx.unlock();

		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		return ptr;
	}
	else
	{
		//通過TLS,每個線程無鎖的獲取自己專屬的ThreadCache對象
		if (pTLSThreadCache == nullptr)
		{
			pTLSThreadCache = new ThreadCache;
		}
		cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;

		return pTLSThreadCache->Allocate(size);
	}
}

也就是說,申請大于256KB的內存時,會直接調用page cache當中的NewSpan函數進行申請,因此這里我們需要再對NewSpan函數進行改造,當需要申請的內存頁數大于128頁時,就直接向堆申請對應頁數的內存塊。而如果申請的內存頁數是小于128頁的,那就在page cache中進行申請,因此當申請大于256KB的內存調用NewSpan函數時也是需要加鎖的,因為我們可能是在page cache中進行申請的。

//獲取一個k頁的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);
	if (k > NPAGES - 1) //大于128頁直接找堆申請
	{
		void* ptr = SystemAlloc(k);
		Span* span = new Span;
		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;
		//建立頁號與span之間的映射
		_idSpanMap[span->_pageId] = span;
		return span;
	}
	//先檢查第k個桶里面有沒有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}
	//檢查一下后面的桶里面有沒有span,如果有可以將其進行切分
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;
			//在nSpan的頭部切k頁下來
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//將剩下的掛到對應映射的位置
			_spanLists[nSpan->_n].PushFront(nSpan);
			//存儲nSpan的首尾頁號與nSpan之間的映射,方便page cache合并span時進行前后頁的查找
			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;

			//建立頁號與span的映射,方便central cache回收小塊內存時查找對應的span
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}
	//走到這里說明后面沒有大頁的span了,這時就向堆申請一個128頁的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	//盡量避免代碼重復,遞歸調用自己
	return NewSpan(k);
}

釋放過程

當釋放對象時,我們需要判斷釋放對象的大?。?/p>

釋放內存的大小釋放方式
x ≤ 256 K B ( 32 頁 ) x \leq 256KB(32頁) x≤256KB(32頁)釋放給thread cache
32 頁 < x ≤ 128 頁 32 頁< x \leq 128頁 32頁<x≤128頁釋放給page cache
x ≥ 128 頁 x \geq 128頁 x≥128頁釋放給堆

因此當釋放對象時,我們需要先找到該對象對應的span,但是在釋放對象時我們只知道該對象的起始地址。這也就是我們在申請大于256KB的內存時,也要給申請到的內存建立span結構,并建立起始頁號與該span之間的映射關系的原因。此時我們就可以通過釋放對象的起始地址計算出起始頁號,進而通過頁號找到該對象對應的span。

static void ConcurrentFree(void* ptr, size_t size)
{
	if (size > MAX_BYTES) //大于256KB的內存釋放
	{
		Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);

		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

因此page cache在回收span時也需要進行判斷,如果該span的大小是小于等于128頁的,那么直接還給page cache進行了,page cache會嘗試對其進行合并。而如果該span的大小是大于128頁的,那么說明該span是直接向堆申請的,我們直接將這塊內存釋放給堆,然后將這個span結構進行delete就行了。

//釋放空閑的span回到PageCache,并合并相鄰的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	if (span->_n > NPAGES - 1) //大于128頁直接釋放給堆
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		delete span;
		return;
	}
	//對span的前后頁,嘗試進行合并,緩解內存碎片問題
	//1、向前合并
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		//前面的頁號沒有(還未向系統(tǒng)申請),停止向前合并
		if (ret == _idSpanMap.end())
		{
			break;
		}
		//前面的頁號對應的span正在被使用,停止向前合并
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}
		//合并出超過128頁的span無法進行管理,停止向前合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//進行向前合并
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		//將prevSpan從對應的雙鏈表中移除
		_spanLists[prevSpan->_n].Erase(prevSpan);

		delete prevSpan;
	}
	//2、向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		//后面的頁號沒有(還未向系統(tǒng)申請),停止向后合并
		if (ret == _idSpanMap.end())
		{
			break;
		}
		//后面的頁號對應的span正在被使用,停止向后合并
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}
		//合并出超過128頁的span無法進行管理,停止向后合并
		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//進行向后合并
		span->_n += nextSpan->_n;

		//將nextSpan從對應的雙鏈表中移除
		_spanLists[nextSpan->_n].Erase(nextSpan);

		delete nextSpan;
	}
	//將合并后的span掛到對應的雙鏈表當中
	_spanLists[span->_n].PushFront(span);
	//建立該span與其首尾頁的映射
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
	//將該span設置為未被使用的狀態(tài)
	span->_isUse = false;
}

說明一下,直接向堆申請內存時我們調用的接口是VirtualAlloc,與之對應的將內存釋放給堆的接口叫做VirtualFree,而Linux下的brk和mmap對應的釋放接口叫做sbrk和unmmap。此時我們也可以將這些釋放接口封裝成一個叫做SystemFree的接口,當我們需要將內存釋放給堆時直接調用SystemFree即可。

//直接將內存還給堆
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	//linux下sbrk unmmap等
#endif
}

簡單測試

下面我們對大于256KB的申請釋放流程進行簡單的測試:

//找page cache申請
void* p1 = ConcurrentAlloc(257 * 1024); //257KB
ConcurrentFree(p1, 257 * 1024);

//找堆申請
void* p2 = ConcurrentAlloc(129 * 8 * 1024); //129頁
ConcurrentFree(p2, 129 * 8 * 1024);

當申請257KB的內存時,由于257KB的內存按頁向上對齊后是33頁,并沒有大于128頁,因此不會直接向堆進行申請,會向page cache申請內存,但此時page cache當中實際是沒有內存的,最終page cache就會向堆申請一個128頁的span,將其切分成33頁的span和95頁的span,并將33頁的span進行返回。

在這里插入圖片描述

而在釋放內存時,由于該對象的大小大于了256KB,因此不會將其還給thread cache,而是直接調用的page cache當中的釋放接口。

在這里插入圖片描述

由于該對象的大小是33頁,不大于128頁,因此page cache也不會直接將該對象還給堆,而是嘗試對其進行合并,最終就會把這個33頁的span和之前剩下的95頁的span進行合并,最終將合并后的128頁的span掛到第128號桶中。

在這里插入圖片描述

當申請129頁的內存時,由于是大于256KB的,于是還是調用的page cache對應的申請接口,但此時申請的內存同時也大于128頁,因此會直接向堆申請。在申請后還會建立該span與其起始頁號之間的映射,便于釋放時可以通過頁號找到該span。

在這里插入圖片描述

在釋放內存時,通過對象的地址找到其對應的span,從span結構中得知釋放內存的大小大于128頁,于是會將該內存直接還給堆。

在這里插入圖片描述

使用定長內存池配合脫離使用new

tcmalloc是要在高并發(fā)場景下替代malloc進行內存申請的,因此tcmalloc在實現的時,其內部是不能調用malloc函數的,我們當前的代碼中存在通過new獲取到的內存,而new在底層實際上就是封裝了malloc。

為了完全脫離掉malloc函數,此時我們之前實現的定長內存池就起作用了,代碼中使用new時基本都是為Span結構的對象申請空間,而span對象基本都是在page cache層創(chuàng)建的,因此我們可以在PageCache類當中定義一個_spanPool,用于span對象的申請和釋放。

//單例模式
class PageCache
{
public:
	//...
private:
	ObjectPool<Span> _spanPool;
};

然后將代碼中使用new的地方替換為調用定長內存池當中的New函數,將代碼中使用delete的地方替換為調用定長內存池當中的Delete函數。

//申請span對象
Span* span = _spanPool.New();
//釋放span對象
_spanPool.Delete(span);

注意,當使用定長內存池當中的New函數申請Span對象時,New函數通過定位new也是對Span對象進行了初始化的。

此外,每個線程第一次申請內存時都會創(chuàng)建其專屬的thread cache,而這個thread cache目前也是new出來的,我們也需要對其進行替換。

//通過TLS,每個線程無鎖的獲取自己專屬的ThreadCache對象
if (pTLSThreadCache == nullptr)
{
	static std::mutex tcMtx;
	static ObjectPool<ThreadCache> tcPool;
	tcMtx.lock();
	pTLSThreadCache = tcPool.New();
	tcMtx.unlock();
}

這里我們將用于申請ThreadCache類對象的定長內存池定義為靜態(tài)的,保持全局只有一個,讓所有線程創(chuàng)建自己的thread cache時,都在個定長內存池中申請內存就行了。

但注意在從該定長內存池中申請內存時需要加鎖,防止多個線程同時申請自己的ThreadCache對象而導致線程安全問題。

最后在SpanList的構造函數中也用到了new,因為SpanList是帶頭循環(huán)雙向鏈表,所以在構造期間我們需要申請一個span對象作為雙鏈表的頭結點。

//帶頭雙向循環(huán)鏈表
class SpanList
{
public:
	SpanList()
	{
		_head = _spanPool.New();
		_head->_next = _head;
		_head->_prev = _head;
	}
private:
	Span* _head;
	static ObjectPool<Span> _spanPool;
};

由于每個span雙鏈表只需要一個頭結點,因此將這個定長內存池定義為靜態(tài)的,保持全局只有一個,讓所有span雙鏈表在申請頭結點時,都在一個定長內存池中申請內存就行了。

釋放對象時優(yōu)化為不傳對象大小

當我們使用malloc函數申請內存時,需要指明申請內存的大??;而當我們使用free函數釋放內存時,只需要傳入指向這塊內存的指針即可。

而我們目前實現的內存池,在釋放對象時除了需要傳入指向該對象的指針,還需要傳入該對象的大小。

原因如下:

  • 如果釋放的是大于256KB的對象,需要根據對象的大小來判斷這塊內存到底應該還給page cache,還是應該直接還給堆。
  • 如果釋放的是小于等于256KB的對象,需要根據對象的大小計算出應該還給thread cache的哪一個哈希桶。

如果我們也想做到,在釋放對象時不用傳入對象的大小,那么我們就需要建立對象地址與對象大小之間的映射。由于現在可以通過對象的地址找到其對應的span,而span的自由鏈表中掛的都是相同大小的對象。

因此我們可以在Span結構中再增加一個_objSize成員,該成員代表著這個span管理的內存塊被切成的一個個對象的大小。

//管理以頁為單位的大塊內存
struct Span
{
	PAGE_ID _pageId = 0;        //大塊內存起始頁的頁號
	size_t _n = 0;              //頁的數量

	Span* _next = nullptr;      //雙鏈表結構
	Span* _prev = nullptr;

	size_t _objSize = 0;        //切好的小對象的大小
	size_t _useCount = 0;       //切好的小塊內存,被分配給thread cache的計數
	void* _freeList = nullptr;  //切好的小塊內存的自由鏈表

	bool _isUse = false;        //是否在被使用
};

而所有的span都是從page cache中拿出來的,因此每當我們調用NewSpan獲取到一個k頁的span時,就應該將這個span的_objSize保存下來。

Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
span->_objSize = size;

代碼中有兩處,一處是在central cache中獲取非空span時,如果central cache對應的桶中沒有非空的span,此時會調用NewSpan獲取一個k頁的span;另一處是當申請大于256KB內存時,會直接調用NewSpan獲取一個k頁的span。

此時當我們釋放對象時,就可以直接從對象的span中獲取到該對象的大小,準確來說獲取到的是對齊以后的大小。

static void ConcurrentFree(void* ptr)
{
	Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
	size_t size = span->_objSize;
	if (size > MAX_BYTES) //大于256KB的內存釋放
	{
		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->Deallocate(ptr, size);
	}
}

讀取映射關系時的加鎖問題

我們將頁號與span之間的映射關系是存儲在PageCache類當中的,當我們訪問這個映射關系時是需要加鎖的,因為STL容器是不保證線程安全的。

對于當前代碼來說,如果我們此時正在page cache進行相關操作,那么訪問這個映射關系是安全的,因為當進入page cache之前是需要加鎖的,因此可以保證此時只有一個線程在進行訪問。

但如果我們是在central cache訪問這個映射關系,或是在調用ConcurrentFree函數釋放內存時訪問這個映射關系,那么就存在線程安全的問題。因為此時可能其他線程正在page cache當中進行某些操作,并且該線程此時可能也在訪問這個映射關系,因此當我們在page cache外部訪問這個映射關系時是需要加鎖的。

實際就是在調用page cache對外提供訪問映射關系的函數時需要加鎖,這里我們可以考慮使用C++當中的unique_lock,當然你也可以用普通的鎖。

//獲取從對象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT; //頁號

	std::unique_lock<std::mutex> lock(_pageMtx); //構造時加鎖,析構時自動解鎖
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

多線程環(huán)境下對比malloc測試

之前我們只是對代碼進行了一些基礎的單元測試,下面我們在多線程場景下對比malloc進行測試。

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&, k]() {
			std::vector<void*> v;
			v.reserve(ntimes);
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(malloc(16));
					//v.push_back(malloc((16 + i) % 8192 + 1));
				}
				size_t end1 = clock();
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					free(v[i]);
				}
				size_t end2 = clock();
				v.clear();
				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
		});
	}
	for (auto& t : vthread)
	{
		t.join();
	}
	printf("%u個線程并發(fā)執(zhí)行%u輪次,每輪次malloc %u次: 花費:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime);
	printf("%u個線程并發(fā)執(zhí)行%u輪次,每輪次free %u次: 花費:%u ms\n",
		nworks, rounds, ntimes, free_costtime);
	printf("%u個線程并發(fā)malloc&free %u次,總計花費:%u ms\n",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}

void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	std::atomic<size_t> malloc_costtime = 0;
	std::atomic<size_t> free_costtime = 0;
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&]() {
			std::vector<void*> v;
			v.reserve(ntimes);
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(ConcurrentAlloc(16));
					//v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
				}
				size_t end1 = clock();
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					ConcurrentFree(v[i]);
				}
				size_t end2 = clock();
				v.clear();
				malloc_costtime += (end1 - begin1);
				free_costtime += (end2 - begin2);
			}
		});
	}
	for (auto& t : vthread)
	{
		t.join();
	}
	printf("%u個線程并發(fā)執(zhí)行%u輪次,每輪次concurrent alloc %u次: 花費:%u ms\n",
		nworks, rounds, ntimes, malloc_costtime);
	printf("%u個線程并發(fā)執(zhí)行%u輪次,每輪次concurrent dealloc %u次: 花費:%u ms\n",
		nworks, rounds, ntimes, free_costtime);
	printf("%u個線程并發(fā)concurrent alloc&dealloc %u次,總計花費:%u ms\n",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}

int main()
{
	size_t n = 10000;
	cout << "==========================================================" <<
		endl;
	BenchmarkConcurrentMalloc(n, 4, 10);
	cout << endl << endl;
	BenchmarkMalloc(n, 4, 10);
	cout << "==========================================================" <<
		endl;
	return 0;
}

其中測試函數各個參數的含義如下:

  • ntimes:單輪次申請和釋放內存的次數。
  • nworks:線程數。
  • rounds:輪次。

在測試函數中,我們通過clock函數分別獲取到每輪次申請和釋放所花費的時間,然后將其對應累加到malloc_costtime和free_costtime上。最后我們就得到了,nworks個線程跑rounds輪,每輪申請和釋放ntimes次,這個過程申請所消耗的時間、釋放所消耗的時間、申請和釋放總共消耗的時間。

注意,我們創(chuàng)建線程時讓線程執(zhí)行的是lambda表達式,而我們這里在使用lambda表達式時,以值傳遞的方式捕捉了變量k,以引用傳遞的方式捕捉了其他父作用域中的變量,因此我們可以將各個線程消耗的時間累加到一起。

我們將所有線程申請內存消耗的時間都累加到malloc_costtime上, 將釋放內存消耗的時間都累加到free_costtime上,此時malloc_costtime和free_costtime可能被多個線程同時進行累加操作的,所以存在線程安全的問題。鑒于此,我們在定義這兩個變量時使用了atomic類模板,這時對它們的操作就是原子操作了。

固定大小內存的申請和釋放

我們先來測試一下固定大小內存的申請和釋放:

v.push_back(malloc(16));
v.push_back(ConcurrentAlloc(16));

此時4個線程執(zhí)行10輪操作,每輪申請釋放10000次,總共申請釋放了40萬次,運行后可以看到,malloc的效率還是更高的。

在這里插入圖片描述

由于此時我們申請釋放的都是固定大小的對象,每個線程申請釋放時訪問的都是各自thread cache的同一個桶,當thread cache的這個桶中沒有對象或對象太多要歸還時,也都會訪問central cache的同一個桶。此時central cache中的桶鎖就不起作用了,因為我們讓central cache使用桶鎖的目的就是為了,讓多個thread cache可以同時訪問central cache的不同桶,而此時每個thread cache訪問的卻都是central cache中的同一個桶。

不同大小內存的申請和釋放

下面我們再來測試一下不同大小內存的申請和釋放:

v.push_back(malloc((16 + i) % 8192 + 1));
v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));

運行后可以看到,由于申請和釋放內存的大小是不同的,此時central cache當中的桶鎖就起作用了,ConcurrentAlloc的效率也有了較大增長,但相比malloc來說還是差一點點。

在這里插入圖片描述

復雜問題的調試技巧

多線程調試比單線程調試要復雜得多,調試時各個線程之間會相互切換,并且每次調試切換的時機也是不固定的,這就使得調試過程變得非常難以控制。

下面給出三個調試時的小技巧:

1、條件斷點

一般情況下我們可以直接運行程序,通過報錯來查找問題。如果此時報的是斷言錯誤,那么我們可以直接定位到報錯的位置,然后將此處的斷言改為與斷言條件相反的if判斷,在if語句里面打上一個斷點,但注意空語句是無法打斷點的,這時我們隨便在if里面加上一句代碼就可以打斷點了。

在這里插入圖片描述

此外,條件斷點也可以通過右擊普通斷點來進行設置。

在這里插入圖片描述

右擊后即可設置相應的條件,程序運行到此處時如果滿足該條件則會停下來。

在這里插入圖片描述

運行到條件斷點處后,我們就可以對當前程序進行進一步分析,找出斷言錯誤的被觸發(fā)原因。

2、查看函數棧幀

當程序運行到斷點處時,我們需要對當前位置進行分析,如果檢查后發(fā)現當前函數是沒有問題的,這時可能需要回到調用該函數的地方進行進一步分析,此時我們可以依次點擊“調試→窗口→調用堆棧”。

在這里插入圖片描述

此時我們就可以看到當前函數棧幀的調用情況,其中黃色箭頭指向的是當前所在的函數棧幀。

在這里插入圖片描述

雙擊函數棧幀中的其他函數,就可以跳轉到該函數對應的棧幀,此時淺灰色箭頭指向的就是當前跳轉到的函數棧幀。

在這里插入圖片描述

需要注意的是,監(jiān)視窗口只能查看當前棧幀中的變量。如果要查看此時其他函數棧幀中變量的情況,就可以通過函數棧幀跳轉來查看。

3、疑似死循環(huán)時中斷程序

當你在某個地方設置斷點后,如果遲遲沒有運行到斷點處,而程序也沒有崩潰,這時有可能是程序進入到某個死循環(huán)了。

在這里插入圖片描述

這時我們可以依次點擊“調試→全部中斷”。

在這里插入圖片描述

這時程序就會在當前運行的地方停下來。

在這里插入圖片描述

性能瓶頸分析

經過前面的測試可以看到,我們的代碼此時與malloc之間還是有差距的,此時我們就應該分析分析我們當前項目的瓶頸在哪里,但這不能簡單的憑感覺,我們應該用性能分析的工具來進行分析。

VS編譯器下性能分析的操作步驟

VS編譯器中就帶有性能分析的工具的,我們可以依次點擊“調試→性能和診斷”進行性能分析,注意該操作要在Debug模式下進行。

在這里插入圖片描述

同時我們將代碼中n的值由10000調成了1000,否則該分析過程可能會花費較多時間,并且將malloc的測試代碼進行了屏蔽,因為我們要分析的是我們實現的高并發(fā)內存池。

int main()
{
	size_t n = 1000;
	cout << "==========================================================" <<
		endl;
	BenchmarkConcurrentMalloc(n, 4, 10);
	cout << endl << endl;
	//BenchmarkMalloc(n, 4, 10);
	cout << "==========================================================" <<
		endl;
	return 0;
}

在點擊了“調試→性能和診斷”后會彈出一個提示框,我們直接點擊“開始”進行了。

在這里插入圖片描述

然后會彈出一個選項框,這里我們選擇的是第二個,因為我們要分析的是各個函數的用時時間,然后點擊下一步。

在這里插入圖片描述

出現以下選項框繼續(xù)點擊下一步。

在這里插入圖片描述

最后點擊完成,就可以等待分析結果了。

在這里插入圖片描述

分析性能瓶頸

通過分析結果可以看到,光是Deallocate和MapObjectToSpan這兩個函數就占用了一半多的時間。

在這里插入圖片描述

而在Deallocate函數中,調用ListTooLong函數時消耗的時間是最多的。

在這里插入圖片描述

繼續(xù)往下看,在ListTooLong函數中,調用ReleaseListToSpans函數時消耗的時間是最多的。

在這里插入圖片描述

再進一步看,在ReleaseListToSpans函數中,調用MapObjectToSpan函數時消耗的時間是最多的。

在這里插入圖片描述

也就是說,最終消耗時間最多的實際就是MapObjectToSpan函數,我們這時再來看看為什么調用MapObjectToSpan函數會消耗這么多時間。通過觀察我們最終發(fā)現,調用該函數時會消耗這么多時間就是因為鎖的原因。

在這里插入圖片描述

因此當前項目的瓶頸點就在鎖競爭上面,需要解決調用MapObjectToSpan函數訪問映射關系時的加鎖問題。tcmalloc當中針對這一點使用了基數樹進行優(yōu)化,使得在讀取這個映射關系時可以做到不加鎖。

針對性能瓶頸使用基數樹進行優(yōu)化

基數樹實際上就是一個分層的哈希表,根據所分層數不同可分為單層基數樹、二層基數樹、三層基數樹等。

單層基數樹

單層基數樹實際采用的就是直接定址法,每一個頁號對應span的地址就存儲數組中在以該頁號為下標的位置。

在這里插入圖片描述

最壞的情況下我們需要建立所有頁號與其span之間的映射關系,因此這個數組中元素個數應該與頁號的數目相同,數組中每個位置存儲的就是對應span的指針。

//單層基數樹
template <int BITS>
class TCMalloc_PageMap1
{
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap1()
	{
		size_t size = sizeof(void*) << BITS; //需要開辟數組的大小
		size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT); //按頁對齊后的大小
		array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT); //向堆申請空間
		memset(array_, 0, size); //對申請到的內存進行清理
	}
	void* get(Number k) const
	{
		if ((k >> BITS) > 0) //k的范圍不在[0, 2^BITS-1]
		{
			return NULL;
		}
		return array_[k]; //返回該頁號對應的span
	}
	void set(Number k, void* v)
	{
		assert((k >> BITS) == 0); //k的范圍必須在[0, 2^BITS-1]
		array_[k] = v; //建立映射
	}
private:
	void** array_; //存儲映射關系的數組
	static const int LENGTH = 1 << BITS; //頁的數目
};

此時當我們需要建立映射時就調用set函數,需要讀取映射關系時,就調用get函數就行了。

代碼中的非類型模板參數BITS表示存儲頁號最多需要比特位的個數。在32位下我們傳入的是32-PAGE_SHIFT,在64位下傳入的是64-PAGE_SHIFT。而其中的LENGTH成員代表的就是頁號的數目,即 2 B I T S 2^{BITS} 2BITS。

比如32位平臺下,以一頁大小為8K為例,此時頁的數目就是 2 32 ÷ 2 13 = 2 19 2^{32}\div2^{13}=2^{19} 232÷213=219,因此存儲頁號最多需要19個比特位,此時傳入非類型模板參數的值就是 32 − 13 = 19 32-13=19 32−13=19。由于32位平臺下指針的大小是4字節(jié),因此該數組的大小就是 2 19 × 4 = 2 21 = 2 M 2^{19}\times4=2^{21}=2M 219×4=221=2M,內存消耗不大,是可行的。但如果是在64位平臺下,此時該數組的大小是 2 51 × 8 = 2 54 = 2 24 G 2^{51}\times8=2^{54}=2^{24}G 251×8=254=224G,這顯然是不可行的,實際上對于64位的平臺,我們需要使用三層基數樹。

二層基數樹

這里還是以32位平臺下,一頁的大小為8K為例來說明,此時存儲頁號最多需要19個比特位。而二層基數樹實際上就是把這19個比特位分為兩次進行映射。

比如用前5個比特位在基數樹的第一層進行映射,映射后得到對應的第二層,然后用剩下的比特位在基數樹的第二層進行映射,映射后最終得到該頁號對應的span指針。

在這里插入圖片描述

在二層基數樹中,第一層的數組占用 2 5 × 4 = 2 7 B y t e 2^{5}\times4=2^{7}Byte 25×4=27Byte空間,第二層的數組最多占用 2 5 × 2 14 × 4 = 2 21 = 2 M 2^{5}\times2^{14}\times4=2^{21}=2M 25×214×4=221=2M。二層基數樹相比一層基數樹的好處就是,一層基數樹必須一開始就把 2 M 2M 2M的數組開辟出來,而二層基數樹一開始時只需將第一層的數組開辟出來,當需要進行某一頁號映射時再開辟對應的第二層的數組就行了。

//二層基數樹
template <int BITS>
class TCMalloc_PageMap2
{
private:
	static const int ROOT_BITS = 5;                //第一層對應頁號的前5個比特位
	static const int ROOT_LENGTH = 1 << ROOT_BITS; //第一層存儲元素的個數
	static const int LEAF_BITS = BITS - ROOT_BITS; //第二層對應頁號的其余比特位
	static const int LEAF_LENGTH = 1 << LEAF_BITS; //第二層存儲元素的個數
	//第一層數組中存儲的元素類型
	struct Leaf
	{
		void* values[LEAF_LENGTH];
	};
	Leaf* root_[ROOT_LENGTH]; //第一層數組
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap2()
	{
		memset(root_, 0, sizeof(root_)); //將第一層的空間進行清理
		PreallocateMoreMemory(); //直接將第二層全部開辟
	}
	void* get(Number k) const
	{
		const Number i1 = k >> LEAF_BITS;        //第一層對應的下標
		const Number i2 = k & (LEAF_LENGTH - 1); //第二層對應的下標
		if ((k >> BITS) > 0 || root_[i1] == NULL) //頁號值不在范圍或沒有建立過映射
		{
			return NULL;
		}
		return root_[i1]->values[i2]; //返回該頁號對應span的指針
	}
	void set(Number k, void* v)
	{
		const Number i1 = k >> LEAF_BITS;        //第一層對應的下標
		const Number i2 = k & (LEAF_LENGTH - 1); //第二層對應的下標
		assert(i1 < ROOT_LENGTH);
		root_[i1]->values[i2] = v; //建立該頁號與對應span的映射
	}
	//確保映射[start,start_n-1]頁號的空間是開辟好了的
	bool Ensure(Number start, size_t n)
	{
		for (Number key = start; key <= start + n - 1;)
		{
			const Number i1 = key >> LEAF_BITS;
			if (i1 >= ROOT_LENGTH) //頁號超出范圍
				return false;
			if (root_[i1] == NULL) //第一層i1下標指向的空間未開辟
			{
				//開辟對應空間
				static ObjectPool<Leaf> leafPool;
				Leaf* leaf = (Leaf*)leafPool.New();
				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS; //繼續(xù)后續(xù)檢查
		}
		return true;
	}
	void PreallocateMoreMemory()
	{
		Ensure(0, 1 << BITS); //將第二層的空間全部開辟好
	}
};

因此在二層基數樹中有一個Ensure函數,當需要建立某一頁號與其span之間的映射關系時,需要先調用該Ensure函數確保用于映射該頁號的空間是開辟了的,如果沒有開辟則會立即開辟。

而在32位平臺下,就算將二層基數樹第二層的數組全部開辟出來也就消耗了 2 M 2M 2M的空間,內存消耗也不算太多,因此我們可以在構造二層基數樹時就把第二層的數組全部開辟出來。

三層基數樹

上面一層基數樹和二層基數樹都適用于32位平臺,而對于64位的平臺就需要用三層基數樹了。三層基數樹與二層基數樹類似,三層基數樹實際上就是把存儲頁號的若干比特位分為三次進行映射。

在這里插入圖片描述

此時只有當要建立某一頁號的映射關系時,再開辟對應的數組空間,而沒有建立映射的頁號就可以不用開辟其對應的數組空間,此時就能在一定程度上節(jié)省內存空間。

//三層基數樹
template <int BITS>
class TCMalloc_PageMap3
{
private:
	static const int INTERIOR_BITS = (BITS + 2) / 3;       //第一、二層對應頁號的比特位個數
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS; //第一、二層存儲元素的個數
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS; //第三層對應頁號的比特位個數
	static const int LEAF_LENGTH = 1 << LEAF_BITS;         //第三層存儲元素的個數
	struct Node
	{
		Node* ptrs[INTERIOR_LENGTH];
	};
	struct Leaf
	{
		void* values[LEAF_LENGTH];
	};
	Node* NewNode()
	{
		static ObjectPool<Node> nodePool;
		Node* result = nodePool.New();
		if (result != NULL)
		{
			memset(result, 0, sizeof(*result));
		}
		return result;
	}
	Node* root_;
public:
	typedef uintptr_t Number;
	explicit TCMalloc_PageMap3()
	{
		root_ = NewNode();
	}
	void* get(Number k) const
	{
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);         //第一層對應的下標
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二層對應的下標
		const Number i3 = k & (LEAF_LENGTH - 1);                    //第三層對應的下標
		//頁號超出范圍,或映射該頁號的空間未開辟
		if ((k >> BITS) > 0 || root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL)
		{
			return NULL;
		}
		return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3]; //返回該頁號對應span的指針
	}
	void set(Number k, void* v)
	{
		assert(k >> BITS == 0);
		const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);         //第一層對應的下標
		const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二層對應的下標
		const Number i3 = k & (LEAF_LENGTH - 1);                    //第三層對應的下標
		Ensure(k, 1); //確保映射第k頁頁號的空間是開辟好了的
		reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v; //建立該頁號與對應span的映射
	}
	//確保映射[start,start+n-1]頁號的空間是開辟好了的
	bool Ensure(Number start, size_t n)
	{
		for (Number key = start; key <= start + n - 1;)
		{
			const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS);         //第一層對應的下標
			const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1); //第二層對應的下標
			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH) //下標值超出范圍
				return false;
			if (root_->ptrs[i1] == NULL) //第一層i1下標指向的空間未開辟
			{
				//開辟對應空間
				Node* n = NewNode();
				if (n == NULL) return false;
				root_->ptrs[i1] = n;
			}
			if (root_->ptrs[i1]->ptrs[i2] == NULL) //第二層i2下標指向的空間未開辟
			{
				//開辟對應空間
				static ObjectPool<Leaf> leafPool;
				Leaf* leaf = leafPool.New();
				if (leaf == NULL) return false;
				memset(leaf, 0, sizeof(*leaf));
				root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
			}
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS; //繼續(xù)后續(xù)檢查
		}
		return true;
	}
	void PreallocateMoreMemory()
	{}
};

因此當我們要建立某一頁號的映射關系時,需要先確保存儲該頁映射的數組空間是開辟好了的,也就是調用代碼中的Ensure函數,如果對應數組空間未開辟則會立馬開辟對應的空間。

使用基數樹進行優(yōu)化代碼實現

代碼更改

現在我們用基數樹對代碼進行優(yōu)化,此時將PageCache類當中的unorder_map用基數樹進行替換即可,由于當前是32位平臺,因此這里隨便用幾層基數樹都可以。

//單例模式
class PageCache
{
public:
	//...
private:
	//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
	TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
};

此時當我們需要建立頁號與span的映射時,就調用基數樹當中的set函數。

_idSpanMap.set(span->_pageId, span);

而當我們需要讀取某一頁號對應的span時,就調用基數樹當中的get函數。

Span* ret = (Span*)_idSpanMap.get(id);

并且現在PageCache類向外提供的,用于讀取映射關系的MapObjectToSpan函數內部就不需要加鎖了。

//獲取從對象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT; //頁號
	Span* ret = (Span*)_idSpanMap.get(id);
	assert(ret != nullptr);
	return ret;
}

為什么讀取基數樹映射關系時不需要加鎖?

當某個線程在讀取映射關系時,可能另外一個線程正在建立其他頁號的映射關系,而此時無論我們用的是C++當中的map還是unordered_map,在讀取映射關系時都是需要加鎖的。

因為C++中map的底層數據結構是紅黑樹,unordered_map的底層數據結構是哈希表,而無論是紅黑樹還是哈希表,當我們在插入數據時其底層的結構都有可能會發(fā)生變化。比如紅黑樹在插入數據時可能會引起樹的旋轉,而哈希表在插入數據時可能會引起哈希表擴容。此時要避免出現數據不一致的問題,就不能讓插入操作和讀取操作同時進行,因此我們在讀取映射關系的時候是需要加鎖的。

而對于基數樹來說就不一樣了,基數樹的空間一旦開辟好了就不會發(fā)生變化,因此無論什么時候去讀取某個頁的映射,都是對應在一個固定的位置進行讀取的。并且我們不會同時對同一個頁進行讀取映射和建立映射的操作,因為我們只有在釋放對象時才需要讀取映射,而建立映射的操作都是在page cache進行的。也就是說,讀取映射時讀取的都是對應span的_useCount不等于0的頁,而建立映射時建立的都是對應span的_useCount等于0的頁,所以說我們不會同時對同一個頁進行讀取映射和建立映射的操作。

再次對比malloc進行測試

還是同樣的代碼,只不過我們用基數樹對代碼進行了優(yōu)化,這時測試固定大小內存的申請和釋放的結果如下:

在這里插入圖片描述

可以看到,這時就算申請釋放的是固定大小的對象,其效率都是malloc的兩倍。下面在申請釋放不同大小的對象時,由于central cache的桶鎖起作用了,其效率更是變成了malloc的好幾倍。

在這里插入圖片描述

打包成動靜態(tài)庫

實際Google開源的tcmalloc是會直接用于替換malloc的,不同平臺替換的方式不同。比如基于Unix的系統(tǒng)上的glibc,使用了weak alias的方式替換;而對于某些其他平臺,需要使用hook的鉤子技術來做。

對于我們當前實現的項目,可以考慮將其打包成靜態(tài)庫或動態(tài)庫。我們先右擊解決方案資源管理器當中的項目名稱,然后選擇屬性。

在這里插入圖片描述

此時會彈出該選項卡,按照以下圖示就可以選擇將其打包成靜態(tài)庫或動態(tài)庫了。

在這里插入圖片描述

項目源碼

Github:https://github.com/chenlong-xcy/standard-project/tree/main/ConcurrentMemoryPool

到此這篇關于C++高并發(fā)內存池的實現的文章就介紹到這了,更多相關C++高并發(fā)內存池內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 詳解如何將Spire.Doc for C++集成到C++程序中

    詳解如何將Spire.Doc for C++集成到C++程序中

    Spire.Doc for C++是一個專業(yè)的Word庫,供開發(fā)人員在任何類型的C++應用程序中閱讀、創(chuàng)建、編輯、比較和轉換 Word 文檔,本文演示了如何以兩種不同的方式將 Spire.Doc for C++ 集成到您的 C++ 應用程序中,希望對大家有所幫助
    2023-05-05
  • C++中產生臨時對象的情況及其解決方案

    C++中產生臨時對象的情況及其解決方案

    這篇文章主要介紹了C++中產生臨時對象的情況及其解決方案,以值傳遞的方式給函數傳參,類型轉換以及函數需要返回對象時,并給對應給出了詳細的解決方案,通過圖文結合的方式講解的非常詳細,需要的朋友可以參考下
    2024-05-05
  • C語言中的const如何保證變量不被修改

    C語言中的const如何保證變量不被修改

    這篇文章主要給大家介紹了關于C語言中const如何保證變量不被修改的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-04-04
  • C++ 虛函數專題

    C++ 虛函數專題

    這篇文章主要介紹了C++中虛函數的知識點,文中配合代碼講解非常細致,供大家參考和學習,感興趣的朋友可以了解下
    2020-06-06
  • C++?多繼承詳情介紹

    C++?多繼承詳情介紹

    這篇文章主要介紹了C++?多繼承詳情,C++支持多繼承,即允許一個類同時繼承多個類。只有C++等少數語言支持多繼承,下面我們就來看看具體的多繼承介紹吧,需要的朋友可以參考一下
    2022-03-03
  • VC基于ADO技術訪問數據庫的方法

    VC基于ADO技術訪問數據庫的方法

    這篇文章主要介紹了VC基于ADO技術訪問數據庫的方法,較為詳細的分析了VC使用ADO操作數據庫的相關實現技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-10-10
  • C語言時間處理實例分享

    C語言時間處理實例分享

    這篇文章主要介紹了C語言時間處理實例分享的相關資料,需要的朋友可以參考下
    2015-07-07
  • QT實現提示右下角冒泡效果

    QT實現提示右下角冒泡效果

    這篇文章主要為大家詳細介紹了QT實現提示右下角冒泡效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-08-08
  • 淺談stringstream 的.str()正確用法和清空操作

    淺談stringstream 的.str()正確用法和清空操作

    下面小編就為大家?guī)硪黄獪\談stringstream 的.str()正確用法和清空操作。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-12-12
  • C++實現寵物商店信息管理系統(tǒng)

    C++實現寵物商店信息管理系統(tǒng)

    這篇文章主要為大家詳細介紹了C++實現寵物商店信息管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03

最新評論

超碰97人人做人人爱| 天天日夜夜干天天操| 中文字幕一区二 区二三区四区| 成人av亚洲一区二区| 韩国女主播精品视频网站| yy96视频在线观看| 亚洲一区二区久久久人妻| 少妇人妻二三区视频| 国产普通话插插视频| 天堂中文字幕翔田av| 国产极品精品免费视频| 美女 午夜 在线视频| 日本最新一二三区不卡在线| av网站色偷偷婷婷网男人的天堂| 韩国一级特黄大片做受| 亚洲一区二区三区久久午夜| 日韩特级黄片高清在线看| 激情综合治理六月婷婷| 青草久久视频在线观看| 2021最新热播中文字幕| 亚洲精品 欧美日韩| 五十路av熟女松本翔子| 丝袜美腿视频诱惑亚洲无| 男人的天堂在线黄色| 中文字幕亚洲中文字幕| 国产精品一区二区三区蜜臀av| 一区二区三区蜜臀在线| 777奇米久久精品一区| 都市激情校园春色狠狠| aiss午夜免费视频| 少妇与子乱在线观看| 国产91精品拍在线观看| 最新91精品视频在线| 黄色男人的天堂视频| 午夜的视频在线观看| 国产精品亚洲а∨天堂免| 婷婷五月亚洲综合在线| 中国黄片视频一区91| 可以在线观看的av中文字幕| 动漫黑丝美女的鸡巴| 五月色婷婷综合开心网4438| 亚洲 人妻 激情 中文| 五十路熟女人妻一区二| 国产成人一区二区三区电影网站| 最新欧美一二三视频| av在线shipin| 中出中文字幕在线观看| 日本三极片中文字幕| 日韩av有码一区二区三区4| 婷婷久久久久深爱网| 欧美精品资源在线观看| 91色秘乱一区二区三区| 超碰97免费人妻麻豆| 久久久久只精品国产三级| 99久久超碰人妻国产| 国产精品国产三级国产精东| 亚洲精品午夜久久久久| 色呦呦视频在线观看视频| 97超碰国语国产97超碰| 日本人妻精品久久久久久| 日韩美女福利视频网| 久久尻中国美女视频| 亚洲男人的天堂a在线| 亚洲少妇高潮免费观看| 久草视频在线免播放| 含骚鸡巴玩逼逼视频| 天天插天天狠天天操| 喷水视频在线观看这里只有精品| 2022精品久久久久久中文字幕| 丝袜长腿第一页在线| 性色蜜臀av一区二区三区| www骚国产精品视频| 亚洲欧美在线视频第一页| 午夜影院在线观看视频羞羞羞| 天堂女人av一区二区| 亚洲精品无码色午夜福利理论片| 伊人精品福利综合导航| 热久久只有这里有精品| 成人av天堂丝袜在线观看 | 五月激情婷婷久久综合网| 好太好爽好想要免费| 男人在床上插女人视频| 欧美少妇性一区二区三区| 欧美视频综合第一页| 日韩无码国产精品强奸乱伦| 老司机午夜精品视频资源 | 天天操,天天干,天天射| 狠狠鲁狠狠操天天晚上干干| 成人24小时免费视频| 一区二区三区四区视频| 亚洲一区二区三区五区| 都市家庭人妻激情自拍视频| 日本免费一级黄色录像| 99热久久极品热亚洲| 婷婷激情四射在线观看视频| 亚洲一区二区三区uij| 999热精品视频在线| 姐姐的朋友2在线观看中文字幕| 桃色视频在线观看一区二区| 丝袜美腿欧美另类 中文字幕| 熟女妇女老妇一二三区| 2025年人妻中文字幕乱码在线| 五月天久久激情视频| 亚洲1区2区3区精华液| 国产午夜亚洲精品不卡在线观看| 青青草国内在线视频精选| 国产在线观看黄色视频| 国产视频一区在线观看| 国产综合精品久久久久蜜臀| 少妇系列一区二区三区视频| 国产一区二区神马久久| 国产乱弄免费视频观看| av日韩在线观看大全| 激情五月婷婷免费视频| 国际av大片在线免费观看| 啪啪啪18禁一区二区三区 | 国产在线免费观看成人| 中文字幕在线一区精品| 成人蜜臀午夜久久一区| 插逼视频双插洞国产操逼插洞| 久久精品亚洲成在人线a| 天天日天天天天天天天天天天| 亚洲在线免费h观看网站| 中文字幕国产专区欧美激情| 亚洲av天堂在线播放| 在线观看成人国产电影| 啪啪啪啪啪啪啪免费视频| 日曰摸日日碰夜夜爽歪歪| 四川五十路熟女av| 91精品综合久久久久3d动漫| 亚洲av日韩精品久久久| 午夜激情久久不卡一区二区| 日本精品一区二区三区在线视频。| 欧美精品伦理三区四区| 在线观看免费av网址大全| 黄色av网站免费在线| 欧美成人黄片一区二区三区| 精品91高清在线观看| 日本美女成人在线视频| 少妇与子乱在线观看| 成年女人免费播放视频| 黑人性生活视频免费看| 亚洲推理片免费看网站| 免费在线观看视频啪啪| 丰满熟女午夜福利视频| 日本裸体熟妇区二区欧美| 做爰视频毛片下载蜜桃视频1| 五月婷婷在线观看视频免费| 国产日韩av一区二区在线| 国产精彩福利精品视频| 成熟丰满熟妇高潮xx×xx| 国产精品亚洲在线观看| 亚洲码av无色中文| 日本五十路熟新垣里子| AV无码一区二区三区不卡| 午夜婷婷在线观看视频| 欧美色呦呦最新网址| 91中文字幕最新合集| 婷婷综合蜜桃av在线| 欧美精品久久久久久影院| 日本熟妇色熟妇在线观看| 喷水视频在线观看这里只有精品| 人妻少妇一区二区三区蜜桃| 亚洲的电影一区二区三区| 国产日韩精品免费在线| 日韩中文字幕在线播放第二页| 亚洲国产欧美一区二区丝袜黑人| 大胸性感美女羞爽操逼毛片| 亚洲天堂成人在线观看视频网站| 亚洲精品欧美日韩在线播放| 欧美色婷婷综合在线| 经典亚洲伊人第一页| 91综合久久亚洲综合| 日韩无码国产精品强奸乱伦| 班长撕开乳罩揉我胸好爽| 亚洲免费va在线播放| 男人操女人的逼免费视频| 偷拍自拍亚洲美腿丝袜| 最新欧美一二三视频| 在线国产精品一区二区三区| 成人av久久精品一区二区| 99精品久久久久久久91蜜桃| 很黄很污很色的午夜网站在线观看 | 涩爱综合久久五月蜜臀| 新97超碰在线观看| 午夜91一区二区三区| 久久精品国产23696| 93精品视频在线观看| 精品亚洲国产中文自在线| 成年人黄色片免费网站| av中文字幕在线导航| 午夜福利资源综合激情午夜福利资| 久久永久免费精品人妻专区| 啊啊啊想要被插进去视频| 日韩av大胆在线观看| 中文字幕人妻三级在线观看| 欧美日本在线视频一区| 综合激情网激情五月五月婷婷| 欧洲精品第一页欧洲精品亚洲| 国产成人小视频在线观看无遮挡 | av一本二本在线观看| av中文在线天堂精品| 国产成人小视频在线观看无遮挡 | 中文字幕网站你懂的| 1区2区3区4区视频在线观看| 人妻熟女在线一区二区| 国产va精品免费观看| 摧残蹂躏av一二三区| 国产熟妇乱妇熟色T区| 在线播放一区二区三区Av无码| 亚洲中文字幕乱码区| 国产高潮无码喷水AV片在线观看| 国产97视频在线精品| 91九色porny国产在线| av完全免费在线观看av| 青青青青草手机在线视频免费看 | 日本性感美女三级视频| 青青草国内在线视频精选| 日韩伦理短片在线观看| 伊人综合aⅴ在线网| 日本乱人一区二区三区| 亚洲精品一区二区三区老狼| 国产成人精品久久二区91| 在线网站你懂得老司机| 亚洲国产成人最新资源| 久久精品国产999| 亚洲av日韩高清hd| 播放日本一区二区三区电影| 精品人妻一二三区久久| 欧美日韩不卡一区不区二区| 99国产精品窥熟女精品| 午夜在线观看岛国av,com| 国产九色91在线观看精品| 啊啊好慢点插舔我逼啊啊啊视频| 亚洲成人激情视频免费观看了| 亚洲综合色在线免费观看| 色av色婷婷人妻久久久精品高清 | 视频一区二区三区高清在线| 懂色av蜜桃a v| 青春草视频在线免费播放| 亚洲中文字字幕乱码| 老司机深夜免费福利视频在线观看| 伊人成人综合开心网| 亚洲午夜电影之麻豆| 日本男女操逼视频免费看| 2025年人妻中文字幕乱码在线| 亚洲一区制服丝袜美腿| 91免费福利网91麻豆国产精品| 青青青国产片免费观看视频| 久久www免费人成一看片| 国产在线自在拍91国语自产精品 | 免费十精品十国产网站| 操的小逼流水的文章| 又粗又硬又猛又爽又黄的| 91精品啪在线免费| 成年人啪啪视频在线观看| 午夜精品亚洲精品五月色| 中文字幕1卡1区2区3区| 在线免费观看欧美小视频| 在线国产中文字幕视频| 国产女人被做到高潮免费视频| 538精品在线观看视频| 青青尤物在线观看视频网站| 国产女人露脸高潮对白视频| 国产卡一卡二卡三乱码手机| 美女福利视频网址导航| 亚洲2021av天堂| 岛国一区二区三区视频在线| 久青青草视频手机在线免费观看| 亚洲在线一区二区欧美| 欧洲欧美日韩国产在线| 在线免费观看日本伦理| 国产性色生活片毛片春晓精品 | 9久在线视频只有精品| 东游记中文字幕版哪里可以看到| 夜色福利视频在线观看| 午夜的视频在线观看| 免费69视频在线看| 欧美激情精品在线观看| 亚洲国产精品中文字幕网站| 在线观看亚洲人成免费网址| 大鸡吧插入女阴道黄色片| 2012中文字幕在线高清| 亚洲精品午夜久久久久| 亚洲成人黄色一区二区三区| 2017亚洲男人天堂| 亚洲一区二区三区uij| 一本一本久久a久久精品综合不卡| 日韩中文字幕在线播放第二页 | 青青青青在线视频免费观看| 中国视频一区二区三区| 国产成人精品av网站| 人妻熟女在线一区二区| 在线免费观看av日韩| 91小伙伴中女熟女高潮| 亚洲欧洲一区二区在线观看| 免费看国产又粗又猛又爽又黄视频| 婷婷色国产黑丝少妇勾搭AV| 粉嫩欧美美人妻小视频| 亚洲少妇人妻无码精品| 亚洲av午夜免费观看| 91人妻精品久久久久久久网站| 99视频精品全部15| 国产又粗又黄又硬又爽| sw137 中文字幕 在线| 91精品激情五月婷婷在线| 日韩熟女系列一区二区三区| 2022国产综合在线干| 国产视频一区二区午夜| 天天日天天日天天射天天干| 91亚洲精品干熟女蜜桃频道 | 国产成人精品一区在线观看| 精品国产高潮中文字幕| 老鸭窝在线观看一区| 婷婷色国产黑丝少妇勾搭AV| 91p0rny九色露脸熟女| 日韩欧美高清免费在线| 80电影天堂网官网| 999九九久久久精品| 国产av国片精品一区二区| 亚洲国产成人最新资源| 亚洲精品久久综合久| 99国产精品窥熟女精品| 超碰97人人做人人爱| 五十路av熟女松本翔子| 亚洲的电影一区二区三区| 青青热久免费精品视频在线观看| 人人妻人人爽人人澡人人精品| 亚洲av天堂在线播放| 播放日本一区二区三区电影 | 18禁无翼鸟成人在线| 国产刺激激情美女网站| 国产av欧美精品高潮网站| 亚洲精品中文字幕下载| 亚洲激情偷拍一区二区| 美女 午夜 在线视频| av老司机精品在线观看| 热思思国产99re| 97a片免费在线观看| 免费高清自慰一区二区三区网站 | 成人国产小视频在线观看| 丰满的子国产在线观看| 亚洲欧美精品综合图片小说| 国产午夜激情福利小视频在线| 538精品在线观看视频| 国产揄拍高清国内精品对白 | 国产黑丝高跟鞋视频在线播放 | 欧美久久久久久三级网| 38av一区二区三区| 欧美特色aaa大片| 亚洲高清国产拍青青草原| 欧美一区二区三区激情啪啪啪| av在线观看网址av| 欧美日韩国产一区二区三区三州| 18禁无翼鸟成人在线| 天天综合天天综合天天网| 无码中文字幕波多野不卡| 在线观看视频 你懂的| 日日日日日日日日夜夜夜夜夜夜| 日韩美女搞黄视频免费| 天天夜天天日天天日| 日韩伦理短片在线观看| 97人人妻人人澡人人爽人人精品| 亚洲 色图 偷拍 欧美| v888av在线观看视频| 日日夜夜大香蕉伊人| 超碰公开大香蕉97| 丰满少妇翘臀后进式| 午夜在线精品偷拍一区二| 成人av免费不卡在线观看| 日本丰满熟妇大屁股久久| wwwxxx一级黄色片| 在线亚洲天堂色播av电影| 蜜桃臀av蜜桃臀av| 91香蕉成人app下载| 丝袜国产专区在线观看| 中文字幕在线免费第一页| 国产一区二区三免费视频| 黄色中文字幕在线播放| 天天日天天添天天爽| 中文字幕成人日韩欧美| 黑人3p华裔熟女普通话| 视频二区在线视频观看| 新97超碰在线观看| 亚洲激情,偷拍视频| 亚洲美女美妇久久字幕组| 99热这里只有国产精品6| 亚洲一区久久免费视频| 日韩一个色综合导航| 日韩美女精品视频在线观看网站 | 亚洲av香蕉一区区二区三区犇| 日韩美女福利视频网| 久久热这里这里只有精品| 国产在线观看免费人成短视频| 五十路息与子猛烈交尾视频| 中文字幕综合一区二区| 九一传媒制片厂视频在线免费观看| 中文字幕在线欧美精品| 亚洲成高清a人片在线观看| 天天日天天鲁天天操| 1区2区3区4区视频在线观看| 亚洲av黄色在线网站| 高潮视频在线快速观看国家快速| 国产性生活中老年人视频网站| 最新黄色av网站在线观看| 国产视频在线视频播放| 亚洲av成人免费网站| 在线视频免费观看网| 国产九色91在线视频| 国产午夜亚洲精品不卡在线观看| av手机在线免费观看日韩av| 久久久精品国产亚洲AV一| 亚洲av色香蕉一区二区三区| 天天日天天干天天搡| 天天草天天色天天干| 日本www中文字幕| 狠狠躁夜夜躁人人爽天天久天啪| 经典国语激情内射视频| 91极品新人『兔兔』精品新作| 不卡一不卡二不卡三| 亚洲女人的天堂av| 国产美女精品福利在线| 2022天天干天天操| 丰满少妇翘臀后进式| 精品久久久久久久久久中文蒉| av成人在线观看一区| 日韩人妻在线视频免费| 青娱乐最新视频在线| av中文在线天堂精品| 丝袜美腿欧美另类 中文字幕| 大鸡巴插入美女黑黑的阴毛| 91九色国产porny蝌蚪| 国产精品久久综合久久| 熟女国产一区亚洲中文字幕| 中文字幕在线永久免费播放| 日日爽天天干夜夜操| av日韩在线免费播放| 99热99re在线播放| 女生自摸在线观看一区二区三区 | 91亚洲手机在线视频播放| 视频一区 视频二区 视频| 特大黑人巨大xxxx| 小穴多水久久精品免费看| asmr福利视频在线观看| 成年人午夜黄片视频资源| 视频一区 二区 三区 综合| 蜜臀av久久久久久久| 日本性感美女写真视频| 色爱av一区二区三区| 非洲黑人一级特黄片| 热99re69精品8在线播放| 97精品视频在线观看| 最新中文字幕免费视频| 免费观看理论片完整版| 在线观看一区二区三级| 大香蕉玖玖一区2区| 直接观看免费黄网站| 精品一区二区三区欧美| 五十路老熟女码av| 大鸡八强奸视频在线观看| 日本啪啪啪啪啪啪啪| 亚洲成人午夜电影在线观看| 日韩近亲视频在线观看| 欧美一区二区三区四区性视频| 午夜av一区二区三区| 高清一区二区欧美系列| 日本熟女50视频免费| 大黑人性xxxxbbbb| 伊人开心婷婷国产av| 美女骚逼日出水来了| 欧美日本国产自视大全| 韩国爱爱视频中文字幕| 99re6热在线精品| 骚货自慰被发现爆操| 中文字幕乱码av资源| 888亚洲欧美国产va在线播放| 40道精品招牌菜特色| 亚洲av香蕉一区区二区三区犇| 国产97在线视频观看| 男人的天堂av日韩亚洲| 99精品国自产在线人| 亚洲av无乱一区二区三区性色| 在线观看av2025| 天美传媒mv视频在线观看| 韩国黄色一级二级三级| 亚洲国产最大av综合| 天天躁夜夜躁日日躁a麻豆| 男人操女人的逼免费视频| 日本av熟女在线视频| 狠狠躁狠狠爱网站视频| 久久久久久99国产精品| 亚洲国产精品黑丝美女| 日曰摸日日碰夜夜爽歪歪| 蜜臀av久久久久久久| 免费黄页网站4188| 91精品啪在线免费| 欧美偷拍自拍色图片| 午夜久久久久久久精品熟女| 久久免看30视频口爆视频| 特级欧美插插插插插bbbbb| 天天草天天色天天干| 久久国产精品精品美女| 亚洲中文精品字幕在线观看| 看一级特黄a大片日本片黑人| 大鸡吧插逼逼视频免费看| 强行扒开双腿猛烈进入免费版| 日韩剧情片电影在线收看| 国产精品视频男人的天堂| 人妻丝袜精品中文字幕| 99re久久这里都是精品视频| 亚洲另类在线免费观看| 日辽宁老肥女在线观看视频| 久久一区二区三区人妻欧美| 9国产精品久久久久老师| 在线免费视频 自拍| 欧洲国产成人精品91铁牛tv| 五十路丰满人妻熟妇| 欧美亚洲少妇福利视频| 亚洲精品中文字幕下载| 中文字幕午夜免费福利视频| 啪啪啪操人视频在线播放| 欧美日韩人妻久久精品高清国产| 中文字幕人妻三级在线观看| 香港三日本三韩国三欧美三级| 美女日逼视频免费观看| 日韩三级黄色片网站| 亚洲2021av天堂| 亚洲国产成人无码麻豆艾秋| 少妇一区二区三区久久久| 黄色黄色黄片78在线| 可以免费看的www视频你懂的| 93视频一区二区三区| 97人妻夜夜爽二区欧美极品| 亚洲另类在线免费观看| 欧美一区二区三区在线资源| 欧美天堂av无线av欧美| 欧美亚洲偷拍自拍色图| 懂色av之国产精品| 人妻少妇精品久久久久久| free性日本少妇| 狠狠躁夜夜躁人人爽天天天天97| 久久农村老妇乱69系列| 久久久久久久久久久免费女人| 最新激情中文字幕视频| 清纯美女在线观看国产| 激情色图一区二区三区| 91高清成人在线视频| 97国产福利小视频合集| 久久久极品久久蜜桃| 午夜激情高清在线观看| 日韩精品激情在线观看| 97欧洲一区二区精品免费| 黑人性生活视频免费看| 天天干天天操天天插天天日| av一区二区三区人妻| av完全免费在线观看av| 国产一区成人在线观看视频| 777奇米久久精品一区| 岛国青草视频在线观看| 在线成人日韩av电影| 18禁精品网站久久| 亚洲图片偷拍自拍区| 天天综合天天综合天天网| 91麻豆精品91久久久久同性| 午夜激情高清在线观看| 99热99re在线播放| 国产熟妇乱妇熟色T区| 美女张开腿让男生操在线看| 高潮喷水在线视频观看| 国产精品人妻66p| 爱爱免费在线观看视频| 91国产资源在线视频| 人妻丝袜精品中文字幕| 欧美老妇精品另类不卡片| 馒头大胆亚洲一区二区| 水蜜桃国产一区二区三区| 社区自拍揄拍尻屁你懂的 | 无码中文字幕波多野不卡| 亚洲免费在线视频网站| 国产精品熟女久久久久浪潮| 欧美 亚洲 另类综合| 日韩伦理短片在线观看| 小穴多水久久精品免费看| 久草视频在线免播放| 天天操天天操天天碰| 激情五月婷婷免费视频| 美女福利写真在线观看视频| 在线观看免费视频网| 国产精品久久久久久久久福交| 亚洲美女高潮喷浆视频| 亚洲区欧美区另类最新章节| 人人妻人人爽人人添夜| 最近中文2019年在线看| 18禁免费av网站| av手机免费在线观看高潮| 国产自拍在线观看成人| 在线视频这里只有精品自拍| 亚洲熟色妇av日韩熟色妇在线| 欧美性受xx黑人性猛交| 福利视频网久久91| 久久午夜夜伦痒痒想咳嗽P| 人妻丝袜榨强中文字幕| 日韩欧美一级精品在线观看| 任你操视频免费在线观看| 偷拍自拍 中文字幕| av中文字幕福利网| 首之国产AV医生和护士小芳| 日本熟妇色熟妇在线观看| 91在线视频在线精品3| 啊啊啊视频试看人妻| 日本熟妇喷水xxx| 亚洲精品高清自拍av| 青青青青操在线观看免费| 亚洲第一黄色在线观看| 2022天天干天天操| 激情图片日韩欧美人妻| 国产精品一区二区久久久av| 欧美另类重口味极品在线观看| 国产剧情演绎系列丝袜高跟| 丝袜亚洲另类欧美变态| 欧美爆乳肉感大码在线观看| 视频久久久久久久人妻| www天堂在线久久| 97青青青手机在线视频| 天天色天天爱天天爽| 人妻在线精品录音叫床| 国产夫妻视频在线观看免费| 少妇人妻100系列| 国产麻豆精品人妻av| 日本中文字幕一二区视频| 国产一级精品综合av| nagger可以指黑人吗| 亚洲va欧美va人人爽3p| 毛片一级完整版免费| 大鸡吧插逼逼视频免费看 | 亚洲av男人的天堂你懂的| 超pen在线观看视频公开97| 人妻熟女在线一区二区| 老司机在线精品福利视频| 亚洲一区二区三区uij| 亚洲午夜在线视频福利| 亚洲福利天堂久久久久久| 日本韩国在线观看一区二区| 五十路息与子猛烈交尾视频| 一个色综合男人天堂| 亚洲精品无码久久久久不卡 | 蜜桃臀av蜜桃臀av| av天堂中文免费在线| 边摸边做超爽毛片18禁色戒 | av俺也去在线播放| 大学生A级毛片免费视频| 欧美色婷婷综合在线| 亚洲av无女神免非久久| chinese国产盗摄一区二区| 日韩欧美在线观看不卡一区二区 | 都市激情校园春色狠狠| 日本后入视频在线观看| 青青青青在线视频免费观看| 亚洲国产欧美一区二区丝袜黑人| 99亚洲美女一区二区三区| 97人妻人人澡爽人人精品| 精品国产污污免费网站入口自| 三级等保密码要求条款| 亚洲国产精品美女在线观看| 93精品视频在线观看| 在线播放一区二区三区Av无码| 青青草在观免费国产精品| 亚洲第一黄色在线观看| 国产综合精品久久久久蜜臀| 成人国产激情自拍三区| 91‖亚洲‖国产熟女| 老司机免费视频网站在线看| av天堂加勒比在线| 精品国产污污免费网站入口自 | 日本一二三中文字幕| 美女张开两腿让男人桶av| 久久这里有免费精品| 极品粉嫩小泬白浆20p主播| 精品亚洲中文字幕av| 亚洲欧美成人综合视频| 国产成人精品一区在线观看| 天天插天天色天天日| 啪啪啪啪啪啪啪啪啪啪黄色| 日日日日日日日日夜夜夜夜夜夜| 97超碰最新免费在线观看| 国产在线自在拍91国语自产精品| 少妇人妻真实精品视频| 性感美女诱惑福利视频| 摧残蹂躏av一二三区| 熟妇一区二区三区高清版| 在线观看日韩激情视频| 成人色综合中文字幕| 国产精品黄片免费在线观看| 久久久极品久久蜜桃| 午夜大尺度无码福利视频| 99精品国产自在现线观看| 亚洲av日韩av网站| 久久久久久99国产精品| 成人乱码一区二区三区av| 91人妻精品一区二区久久| 岛国一区二区三区视频在线| 97年大学生大白天操逼| 免费av岛国天堂网站| 在线观看一区二区三级| 日韩三级黄色片网站| 国产黄网站在线观看播放| 国产av福利网址大全| 亚洲 色图 偷拍 欧美| 偷拍3456eee| 成年人的在线免费视频| 天天干狠狠干天天操| 一区二区在线视频中文字幕| 好了av中文字幕在线| 经典亚洲伊人第一页| 日韩美女精品视频在线观看网站| 亚洲av成人免费网站| 毛片av在线免费看| 国产亚洲精品品视频在线| 欧美成人精品欧美一级黄色| 91天堂精品一区二区| 欧美viboss性丰满| 一区二区三区激情在线| 天天射,天天操,天天说| 国产熟妇人妻ⅹxxxx麻豆| 9l人妻人人爽人人爽| 中文字幕高清在线免费播放| 一区二区三区四区视频| 自拍偷拍亚洲另类色图| 99精品一区二区三区的区| 欧美精品亚洲精品日韩在线| 成人久久精品一区二区三区| 国产免费av一区二区凹凸四季| 夜夜嗨av蜜臀av| 青青色国产视频在线| 高清成人av一区三区| 亚洲国产成人av在线一区| 亚洲国产免费av一区二区三区| 青草久久视频在线观看| 成人网18免费视频版国产| 色爱av一区二区三区| jiuse91九色视频| 黑人巨大的吊bdsm| 亚洲男人在线天堂网| 女同久久精品秋霞网| 久久尻中国美女视频| 亚洲综合自拍视频一区| 国产精品3p和黑人大战| 黄片色呦呦视频免费看| 97年大学生大白天操逼| 在线观看av观看av| av在线观看网址av| 亚洲av人人澡人人爽人人爱| 成人国产激情自拍三区| 日韩无码国产精品强奸乱伦| 日辽宁老肥女在线观看视频| 青青青青草手机在线视频免费看| jiuse91九色视频| 国产无遮挡裸体免费直播视频| 国产av一区2区3区| 欧美日韩熟女一区二区三区| 手机看片福利盒子日韩在线播放| 黄色资源视频网站日韩| 亚洲精品麻豆免费在线观看| 亚洲av香蕉一区区二区三区犇 | 熟女视频一区,二区,三区| 高清一区二区欧美系列| 欧美日韩人妻久久精品高清国产| 成年人免费看在线视频| 馒头大胆亚洲一区二区| avjpm亚洲伊人久久| 摧残蹂躏av一二三区| 在线视频国产欧美日韩| 男女啪啪视频免费在线观看| 欧美在线精品一区二区三区视频| 亚洲中文字幕人妻一区| 一区二区麻豆传媒黄片| 中文字幕最新久久久| 精品国产成人亚洲午夜| 亚洲一区二区三区久久午夜| 2021久久免费视频| 天堂av中文在线最新版| 天天操天天干天天插| gay gay男男瑟瑟在线网站| 热99re69精品8在线播放| 国产福利小视频大全| 天天日天天干天天爱| 国产熟妇一区二区三区av| 一区二区三区四区五区性感视频| 日韩欧美高清免费在线| 顶级尤物粉嫩小尤物网站| 啪啪啪18禁一区二区三区| 91人妻精品一区二区久久| 性生活第二下硬不起来| 9久在线视频只有精品| 手机看片福利盒子日韩在线播放| 狠狠操狠狠操免费视频| 国产+亚洲+欧美+另类| 青青在线视频性感少妇和隔壁黑丝| 日韩av有码中文字幕| 超级福利视频在线观看| 亚洲中文字幕国产日韩| 亚洲av无码成人精品区辽| 久草极品美女视频在线观看| 美女少妇亚洲精选av| 国产精品视频资源在线播放| 免费观看丰满少妇做受| 国产精品黄色的av| 国产九色91在线视频| 日本一二三区不卡无| 偷拍自拍 中文字幕| 五月色婷婷综合开心网4438| 淫秽激情视频免费观看| av网址国产在线观看| 成人sm视频在线观看| 亚洲成a人片777777| 超碰中文字幕免费观看| 成人福利视频免费在线| 国产普通话插插视频| 激情小视频国产在线| 18禁无翼鸟成人在线| 久久久久久久久久久免费女人| 国产亚州色婷婷久久99精品| av老司机亚洲一区二区| 做爰视频毛片下载蜜桃视频1| 91‖亚洲‖国产熟女| av在线免费中文字幕| 欧美日韩情色在线观看| 欧美视频一区免费在线| 视频在线亚洲一区二区| 喷水视频在线观看这里只有精品| 大骚逼91抽插出水视频| 二区中出在线观看老师| 日本黄色特一级视频| 亚洲一区自拍高清免费视频| 国产第一美女一区二区三区四区| 插小穴高清无码中文字幕| 久久精品视频一区二区三区四区| 中国黄色av一级片| 国产在线91观看免费观看| 欧美区一区二区三视频| 97青青青手机在线视频| 在线国产精品一区二区三区| 38av一区二区三区| 亚洲一区av中文字幕在线观看| 国产在线一区二区三区麻酥酥 | 一区二区三区av高清免费| 1区2区3区不卡视频| 欧美xxx成人在线| 青青草原网站在线观看| 午夜美女少妇福利视频| 日韩欧美制服诱惑一区在线| 亚洲激情唯美亚洲激情图片| 五十路息与子猛烈交尾视频| 午夜精品福利91av| 白嫩白嫩美女极品国产在线观看| 亚洲蜜臀av一区二区三区九色 | 91色老99久久九九爱精品| av手机免费在线观看高潮| AV天堂一区二区免费试看| 亚洲一区二区三区五区| 揄拍成人国产精品免费看视频| 大鸡巴操b视频在线| 98视频精品在线观看| 五十路熟女人妻一区二| 一级黄色av在线观看| 五色婷婷综合狠狠爱| 人妻无码中文字幕专区| 亚洲区欧美区另类最新章节| 成人蜜臀午夜久久一区| 日韩成人性色生活片| 沙月文乃人妻侵犯中文字幕在线| 91天堂精品一区二区| 日韩成人性色生活片| 黄色无码鸡吧操逼视频| 五十路人妻熟女av一区二区| 成年人中文字幕在线观看| 岛国毛片视频免费在线观看| 午夜极品美女福利视频| 亚洲日本一区二区三区| 国产午夜无码福利在线看| 人妻无码色噜噜狠狠狠狠色| 亚洲成人国产av在线| 香港一级特黄大片在线播放| 女人精品内射国产99| 91 亚洲视频在线观看| 在线观看国产网站资源| 在线国产日韩欧美视频| 亚洲一区二区三区uij| 任我爽精品视频在线播放| 日韩av大胆在线观看| 国产精品污污污久久| japanese日本熟妇另类| 中文字幕免费在线免费| 香蕉片在线观看av| 丝袜肉丝一区二区三区四区在线看| 亚洲天堂第一页中文字幕| 天堂资源网av中文字幕| 视频一区 视频二区 视频| 欧美麻豆av在线播放| 9国产精品久久久久老师| av天堂中文字幕最新| 男生用鸡操女生视频动漫| 91麻豆精品传媒国产黄色片| 欧美成人猛片aaaaaaa| 男女第一次视频在线观看| 91精品激情五月婷婷在线| 中文字幕国产专区欧美激情| 亚洲欧美日韩视频免费观看| 免费人成黄页网站在线观看国产| 亚洲精品高清自拍av| 中文字幕一区二区三区蜜月| 中文字幕熟女人妻久久久| 久久99久久99精品影院| 中文字幕AV在线免费看 | 亚洲 自拍 色综合图| 韩国AV无码不卡在线播放| 岛国青草视频在线观看| 97资源人妻免费在线视频| 国产福利小视频二区| 日韩少妇人妻精品无码专区| 色呦呦视频在线观看视频| 大尺度激情四射网站| 2o22av在线视频| 全国亚洲男人的天堂| 女同性ⅹxx女同hd| 亚洲一区久久免费视频| 中文字幕日韩精品日本| 绯色av蜜臀vs少妇| 91自产国产精品视频| av视屏免费在线播放| 开心 色 六月 婷婷| 人人妻人人爽人人添夜| 亚洲av在线观看尤物| 偷拍美女一区二区三区| 蝴蝶伊人久久中文娱乐网 | 熟女视频一区,二区,三区| 国产三级片久久久久久久| 真实国模和老外性视频| 极品性荡少妇一区二区色欲| 亚洲欧美另类手机在线| 中文字幕综合一区二区| 国产亚洲欧美视频网站| 亚洲人妻30pwc| 成年人免费看在线视频| 国产欧美精品不卡在线| 日本男女操逼视频免费看 | 日韩欧美亚洲熟女人妻| 国产精品久久久久久美女校花| 日本女大学生的黄色小视频| 国产麻豆国语对白露脸剧情 | 中文字幕人妻熟女在线电影| 久久这里只有精彩视频免费| 操日韩美女视频在线免费看| 91九色porny国产在线| 欧美日韩国产一区二区三区三州| 中文字幕AV在线免费看 | 久久亚洲天堂中文对白| 中文字幕 码 在线视频| 性感美女福利视频网站| 欧美综合婷婷欧美综合| 福利视频广场一区二区| 日本欧美视频在线观看三区| 美女 午夜 在线视频| 黄工厂精品视频在线观看| 欧美亚洲国产成人免费在线| 在线可以看的视频你懂的| 国产不卡av在线免费| 99热国产精品666| 国产视频精品资源网站| 爱有来生高清在线中文字幕| 伊人网中文字幕在线视频| 97青青青手机在线视频| 午夜精品一区二区三区福利视频| 性生活第二下硬不起来| 久久这里有免费精品| 欧美久久久久久三级网| 老师啊太大了啊啊啊尻视频| 国产91久久精品一区二区字幕| 亚洲天堂av最新网址| 少妇深喉口爆吞精韩国| 最近的中文字幕在线mv视频| 91精品综合久久久久3d动漫| 韩国男女黄色在线观看| 天天干夜夜操天天舔| 98精产国品一二三产区区别| 在线免费观看99视频| 绝色少妇高潮3在线观看| 一色桃子久久精品亚洲| 97人妻总资源视频| 在线视频国产欧美日韩| 久久综合老鸭窝色综合久久 | 亚洲老熟妇日本老妇| 欧美一区二区三区久久久aaa| 视频一区二区在线免费播放| 亚洲国产美女一区二区三区软件| 亚洲av无码成人精品区辽| 日韩黄色片在线观看网站| 国产午夜男女爽爽爽爽爽视频| 青娱乐极品视频青青草| 欧美一区二区三区在线资源 | 最新97国产在线视频| 在线观看911精品国产| 2022国产精品视频| 亚洲av无乱一区二区三区性色| 精品国产午夜视频一区二区| 国产在线一区二区三区麻酥酥| 一区二区在线观看少妇| 日本高清成人一区二区三区| 亚洲高清国产拍青青草原| 久久久麻豆精亚洲av麻花| 国产综合视频在线看片| 韩国三级aaaaa高清视频| 亚洲免费va在线播放| 夏目彩春在线中文字幕| 97年大学生大白天操逼| 喷水视频在线观看这里只有精品| 黄色成年网站午夜在线观看| 视频啪啪啪免费观看| 精品视频中文字幕在线播放| 超碰中文字幕免费观看| 亚洲2021av天堂| 黄片大全在线观看观看| 精品老妇女久久9g国产| 国产密臀av一区二区三| 精品国产成人亚洲午夜| 成年午夜影片国产片| 天天干天天操天天摸天天射| 狠狠的往里顶撞h百合| 97人妻无码AV碰碰视频| 日韩在线中文字幕色| 国产亚洲国产av网站在线| 国产亚洲四十路五十路| 亚洲中文字幕人妻一区| 日韩精品一区二区三区在线播放| 天天操天天干天天日狠狠插 | 免费在线看的黄网站| 午夜精品福利91av| 六月婷婷激情一区二区三区| 中文字幕在线欧美精品| 国产亚洲视频在线观看| 新97超碰在线观看| 日韩精品中文字幕在线| 9色精品视频在线观看| 天堂av狠狠操蜜桃| 国产福利在线视频一区| 亚洲免费国产在线日韩| 91国产资源在线视频| 在线观看亚洲人成免费网址| 一区二区三区国产精选在线播放| 国产刺激激情美女网站| 91啪国自产中文字幕在线| 国产精品系列在线观看一区二区| 自拍偷拍一区二区三区图片| 宅男噜噜噜666免费观看| 黄工厂精品视频在线观看| 色噜噜噜噜18禁止观看| 色综合久久无码中文字幕波多| 国产美女精品福利在线| 中文字幕亚洲久久久| 亚洲一区二区三区精品乱码| 成人国产影院在线观看| av黄色成人在线观看| av手机在线观播放网站| av天堂中文免费在线| 成人av天堂丝袜在线观看| 国产性色生活片毛片春晓精品 | 亚洲色偷偷综合亚洲AV伊人| 国产日韩av一区二区在线| 91色老99久久九九爱精品| 肏插流水妹子在线乐播下载| 精品国产污污免费网站入口自| 亚洲精品高清自拍av| 99精品视频在线观看免费播放| 91香蕉成人app下载| 最新国产亚洲精品中文在线| 快点插进来操我逼啊视频| 2020国产在线不卡视频| 久久久久久国产精品| 97青青青手机在线视频| 亚洲蜜臀av一区二区三区九色| 视频一区二区在线免费播放| 精品一区二区三区三区88| 在线视频这里只有精品自拍| 亚洲高清免费在线观看视频| 插逼视频双插洞国产操逼插洞| 成人免费毛片aaaa| 午夜精品福利91av| 1000小视频在线| 中文字幕无码日韩专区免费| 日韩人妻在线视频免费| 激情色图一区二区三区| 含骚鸡巴玩逼逼视频| 国产精品久久9999| av网站色偷偷婷婷网男人的天堂| 丝袜亚洲另类欧美变态| 国产成人一区二区三区电影网站| 亚洲天堂av最新网址| 欧美视频中文一区二区三区| 精品久久婷婷免费视频| 性欧美日本大妈母与子| 又色又爽又黄的美女裸体| 日本黄在免费看视频| 激情伦理欧美日韩中文字幕| 亚洲免费va在线播放| 欧美日韩情色在线观看| 超黄超污网站在线观看| 黄色大片免费观看网站| 岛国青草视频在线观看| 国产熟妇人妻ⅹxxxx麻豆| 在线观看免费视频色97| 亚洲欧美国产麻豆综合| 扒开腿挺进肉嫩小18禁视频| 日本最新一二三区不卡在线| 一二三中文乱码亚洲乱码one| 亚洲国产欧美一区二区三区久久| 中文字幕在线乱码一区二区| 好太好爽好想要免费| 日本一二三中文字幕| 国产女人叫床高潮大片视频| 中文字幕综合一区二区| 亚洲成a人片777777| 国产一区av澳门在线观看| 大鸡巴插入美女黑黑的阴毛| 国产精品一区二区三区蜜臀av | 97色视频在线观看| 老司机99精品视频在线观看 | 狠狠躁夜夜躁人人爽天天天天97| 国产老熟女伦老熟妇ⅹ| 午夜激情高清在线观看| 北条麻妃高跟丝袜啪啪| 亚洲国产最大av综合| sspd152中文字幕在线| 成人国产影院在线观看| 中文字幕av一区在线观看| 阴茎插到阴道里面的视频| 日韩精品激情在线观看| 中文字幕人妻被公上司喝醉在线| 97人妻夜夜爽二区欧美极品| 亚洲中文字幕综合小综合| 国产日韩一区二区在线看| 动漫美女的小穴视频| 久久久精品精品视频视频| 亚洲福利天堂久久久久久| 免费观看污视频网站| 亚洲在线一区二区欧美| 香港一级特黄大片在线播放| 日日夜夜狠狠干视频| 精品久久久久久久久久久久人妻| 不卡日韩av在线观看| 亚洲欧美一卡二卡三卡| 中文字幕一区的人妻欧美日韩| 欧美成人综合视频一区二区| 亚洲男人让女人爽的视频| av网址国产在线观看| 一区二区三区四区五区性感视频 | 91精品啪在线免费| 欧美日韩一区二区电影在线观看| 开心 色 六月 婷婷| 久久精品亚洲国产av香蕉| 欧美性受xx黑人性猛交| 亚洲一区二区三区在线高清| 开心 色 六月 婷婷| 日日摸夜夜添夜夜添毛片性色av| 久草福利电影在线观看| 中文字幕乱码人妻电影| www天堂在线久久| 91福利在线视频免费观看| 久草视频福利在线首页| 国产又色又刺激在线视频| 国产精品福利小视频a| 久久亚洲天堂中文对白| 中国黄片视频一区91| 99热久久极品热亚洲| 亚洲成人熟妇一区二区三区| 在线免费观看黄页视频| 91she九色精品国产| 中文字幕人妻熟女在线电影| 午夜在线观看岛国av,com| 天天干天天操天天爽天天摸| 欧美3p在线观看一区二区三区| 青青青青操在线观看免费| 国产精品久久久久久久久福交| 成年女人免费播放视频| 性色av一区二区三区久久久| 国产又粗又猛又爽又黄的视频在线| 一区国内二区日韩三区欧美| 在线观看视频 你懂的| 激情人妻校园春色亚洲欧美 | 小穴多水久久精品免费看| 中文字幕一区二 区二三区四区| 久久人人做人人妻人人玩精品vr| 国产熟妇人妻ⅹxxxx麻豆| 国产亚洲视频在线二区| 亚洲欧美成人综合在线观看| avjpm亚洲伊人久久| 欧美地区一二三专区| 任我爽精品视频在线播放| 亚洲成高清a人片在线观看| 99精品久久久久久久91蜜桃| 天天色天天操天天舔| 岛国青草视频在线观看| 欧美aa一级一区三区四区| chinese国产盗摄一区二区| 天天操,天天干,天天射| 亚洲欧美在线视频第一页| 国产精品3p和黑人大战| 蜜臀av久久久久蜜臀av麻豆| 日韩精品二区一区久久| 99一区二区在线观看| lutube在线成人免费看| 91麻豆精品久久久久| 久久久久只精品国产三级| 日韩美在线观看视频黄| 午夜免费体验区在线观看| 五月色婷婷综合开心网4438| 小泽玛利亚视频在线观看| 午夜精品一区二区三区城中村| 亚洲精品乱码久久久久久密桃明 | 888欧美视频在线| 亚洲av无码成人精品区辽| 青青在线视频性感少妇和隔壁黑丝 | 国产精品污污污久久| 伊人情人综合成人久久网小说| 国产福利小视频二区| wwwxxx一级黄色片| 国产又粗又硬又大视频| 在线播放 日韩 av| 农村胖女人操逼视频| 亚洲熟妇无码一区二区三区| 亚洲av天堂在线播放| 初美沙希中文字幕在线| 美女小视频网站在线| 日韩视频一区二区免费观看| 日韩a级黄色小视频| 真实国产乱子伦一区二区| 成年美女黄网站18禁久久| 都市激情校园春色狠狠| 国产精品大陆在线2019不卡| 偷拍自拍亚洲视频在线观看| 精品国产成人亚洲午夜| 在线观看911精品国产| 中文字幕乱码av资源| 在线新三级黄伊人网| 久久永久免费精品人妻专区| 日韩熟女av天堂系列| 亚洲va欧美va人人爽3p| 少妇人妻二三区视频 | 久久机热/这里只有| 啊啊啊视频试看人妻| 亚洲av男人天堂久久| 亚洲熟色妇av日韩熟色妇在线| 日韩av大胆在线观看| 一区二区视频在线观看免费观看| 国产janese在线播放| 精品一区二区三区午夜| 制丝袜业一区二区三区| 天天日天天鲁天天操| 人妻3p真实偷拍一二区| 大肉大捧一进一出好爽在线视频| 超级av免费观看一区二区三区| 热久久只有这里有精品| 亚洲中文字幕国产日韩| 国产乱弄免费视频观看| 无套猛戳丰满少妇人妻| 在线观看操大逼视频| 欧美少妇性一区二区三区| 无码国产精品一区二区高潮久久4| 少妇系列一区二区三区视频| 91欧美在线免费观看| 在线观看日韩激情视频| 一色桃子久久精品亚洲| 日本a级视频老女人| 天天摸天天日天天操| 欧美交性又色又爽又黄麻豆| 国内自拍第一页在线观看| 一二三区在线观看视频| 欧美视频不卡一区四区| 免费男阳茎伸入女阳道视频| 美女福利视频网址导航| 亚洲欧美综合另类13p| 人人妻人人爽人人澡人人精品| av老司机精品在线观看| 亚洲福利精品福利精品福利 | 久久精品36亚洲精品束缚| nagger可以指黑人吗| 久久一区二区三区人妻欧美| 日韩欧美一级精品在线观看| 国产亚洲精品品视频在线| okirakuhuhu在线观看| 熟女人妻三十路四十路人妻斩| 婷婷激情四射在线观看视频| 人妻激情图片视频小说| 超碰97人人做人人爱| 五月天色婷婷在线观看视频免费| 男人的天堂一区二区在线观看| 欧美成人猛片aaaaaaa| 日韩av中文在线免费观看| 91色老99久久九九爱精品| 国产黑丝高跟鞋视频在线播放| 在线免费观看视频一二区| 91极品新人『兔兔』精品新作| 视频二区在线视频观看| av老司机精品在线观看| 精品人妻每日一部精品| 国产V亚洲V天堂无码欠欠| 久久久精品精品视频视频| 欧美精品亚洲精品日韩在线| 男女第一次视频在线观看| 大学生A级毛片免费视频| 亚洲熟女久久久36d| 国产亚洲成人免费在线观看 | 亚洲欧美激情中文字幕| 亚洲狠狠婷婷综合久久app| 欧美另类重口味极品在线观看| 国产综合高清在线观看| 亚洲高清国产自产av| 91色秘乱一区二区三区| 第一福利视频在线观看| 欧美一区二区三区激情啪啪啪| 在线观看操大逼视频| 在线免费观看亚洲精品电影| h国产小视频福利在线观看| 蜜桃视频17c在线一区二区| 黄色片年轻人在线观看| 天天射,天天操,天天说| av久久精品北条麻妃av观看| 狠狠地躁夜夜躁日日躁| 国产第一美女一区二区三区四区| 啪啪啪啪啪啪啪啪啪啪黄色| free性日本少妇| 大香蕉日本伊人中文在线| 中文字幕免费福利视频6| 欧美特色aaa大片| 中文字幕1卡1区2区3区| 姐姐的朋友2在线观看中文字幕| 国产揄拍高清国内精品对白| 中文字幕AV在线免费看 | 亚洲精品色在线观看视频| 天堂av在线最新版在线| 福利在线视频网址导航| 97欧洲一区二区精品免费| 人人爱人人妻人人澡39| 99精品一区二区三区的区| 天码人妻一区二区三区在线看| 欧美黑人性猛交xxxxⅹooo| 888欧美视频在线| 插逼视频双插洞国产操逼插洞| 天天爽夜夜爽人人爽QC| 人人妻人人澡人人爽人人dvl| 亚洲伊人久久精品影院一美女洗澡 | 欧美黑人性暴力猛交喷水| 最新97国产在线视频| 97资源人妻免费在线视频| 久草视频在线一区二区三区资源站| 国产精彩对白一区二区三区| 高清一区二区欧美系列| sejizz在线视频| 晚上一个人看操B片| 国产精品自拍偷拍a| 亚洲免费福利一区二区三区| 成人蜜桃美臀九一一区二区三区| 99久久99久国产黄毛片| 在线观看av亚洲情色| 98精产国品一二三产区区别| 黄色视频成年人免费观看| 精品国产成人亚洲午夜| 99re6热在线精品| 欧美日本在线视频一区| 国产极品精品免费视频| 2021最新热播中文字幕| 欧美成人精品在线观看| 国产黄色a级三级三级三级| 国产一区成人在线观看视频 | 熟女人妻在线观看视频| 日本少妇人妻xxxxx18| 久久精品在线观看一区二区| 国产一级麻豆精品免费| 国产亚州色婷婷久久99精品| 精品一区二区三区三区色爱| 91av中文视频在线| 又色又爽又黄的美女裸体| 午夜毛片不卡免费观看视频| 青青草人人妻人人妻| 天堂资源网av中文字幕| 国产精品三级三级三级| 亚洲欧美久久久久久久久| 亚洲欧美清纯唯美另类| 亚洲欧美自拍另类图片| 一区二区三区四区视频在线播放| 男女啪啪视频免费在线观看| 日本脱亚入欧是指什么| 精品老妇女久久9g国产| 九色视频在线观看免费| 久久免看30视频口爆视频| 国产精品三级三级三级| 欧美另类z0z变态| av在线资源中文字幕| 无码精品一区二区三区人| 在线免费观看日本片| 亚洲综合另类欧美久久| 欧美在线精品一区二区三区视频| 91亚洲精品干熟女蜜桃频道| 青青草在观免费国产精品| 天天日天天舔天天射进去| 成人动漫大肉棒插进去视频| 亚洲国产美女一区二区三区软件 | 黑人变态深video特大巨大| 91试看福利一分钟| 日韩近亲视频在线观看| 91国内精品自线在拍白富美| 99久久成人日韩欧美精品| 又粗又硬又猛又爽又黄的| 一区二区三区综合视频| 大白屁股精品视频国产| 18禁美女无遮挡免费| 欧美精品久久久久久影院| 四川乱子伦视频国产vip| 青青青青青手机视频| av俺也去在线播放| 精品视频一区二区三区四区五区| 国产日本精品久久久久久久| 特级欧美插插插插插bbbbb| 香蕉aⅴ一区二区三区| 狠狠嗨日韩综合久久| av网站色偷偷婷婷网男人的天堂| 日本一二三中文字幕| 自拍偷拍,中文字幕| 91精品国产黑色丝袜| 日韩激情文学在线视频| 夜色撩人久久7777| 国产超码片内射在线| 国产日韩一区二区在线看| 人妻少妇中文有码精品| 国产精品久久久久久久女人18| chinese国产盗摄一区二区| 久久久久五月天丁香社区| 91试看福利一分钟| 99热久久这里只有精品| 日本熟妇一区二区x x| 自拍偷拍,中文字幕| 亚洲嫩模一区二区三区| 熟妇一区二区三区高清版| 亚洲中文字幕人妻一区| 熟女少妇激情五十路| 深田咏美亚洲一区二区| 日本特级片中文字幕| 国产一级精品综合av| 激情内射在线免费观看| 色伦色伦777国产精品| 青青青青青青草国产| 国产精品视频男人的天堂| 天天操天天操天天碰| 亚洲国产成人在线一区| 欧美激情精品在线观看| 超级碰碰在线视频免费观看| 日本性感美女三级视频| 激情人妻校园春色亚洲欧美| 97超碰国语国产97超碰| 91免费福利网91麻豆国产精品| 视频二区在线视频观看| 日本少妇精品免费视频| 一区二区熟女人妻视频| 78色精品一区二区三区| 午夜精品一区二区三区4| 青青青青青青青青青国产精品视频| 欧美另类z0z变态| 2022国产综合在线干| 青青色国产视频在线| 中文字幕第三十八页久久| 亚洲中文字幕国产日韩| 在线观看的a站 最新| 欧美一区二区三区乱码在线播放| 九一传媒制片厂视频在线免费观看 | 2018最新中文字幕在线观看| 91片黄在线观看喷潮| 91av精品视频在线| 日韩人妻xxxxx| 亚洲av日韩av第一区二区三区| 亚洲另类伦春色综合小| 5528327男人天堂| 男女第一次视频在线观看| 中文字幕最新久久久| 污污小视频91在线观看| 超碰97人人澡人人| 91中文字幕免费在线观看| 久草视频在线一区二区三区资源站| 欧美日韩国产一区二区三区三州| 国产揄拍高清国内精品对白 | 四虎永久在线精品免费区二区| 婷婷六月天中文字幕| 欧美日韩中文字幕欧美| 狠狠地躁夜夜躁日日躁| 视频一区二区在线免费播放| 亚洲av极品精品在线观看| 一区二区三区毛片国产一区| 欧美色婷婷综合在线| 乱亲女秽乱长久久久| 成人精品视频99第一页| 激情内射在线免费观看| 久草免费人妻视频在线| 狠狠躁夜夜躁人人爽天天天天97| 五月天中文字幕内射| 中文字幕无码一区二区免费| 中文字幕高清免费在线人妻| 日韩中文字幕福利av| 九色视频在线观看免费| 成人蜜桃美臀九一一区二区三区| 亚洲精品乱码久久久久久密桃明| 欧美日韩在线精品一区二区三| 亚洲另类综合一区小说| 婷婷午夜国产精品久久久| 偷拍自拍国产在线视频| 999久久久久999| 亚洲综合色在线免费观看| 亚洲国产成人av在线一区| 天天摸天天亲天天舔天天操天天爽| 久碰精品少妇中文字幕av | 美女少妇亚洲精选av| 91中文字幕免费在线观看| 在线免费观看99视频| 国产午夜亚洲精品麻豆| 国产又色又刺激在线视频| 丰满的子国产在线观看| 77久久久久国产精产品| 天天草天天色天天干| 成年午夜影片国产片| 午夜美女福利小视频| 色爱av一区二区三区| 婷婷综合亚洲爱久久| 精品成人啪啪18免费蜜臀| 色哟哟国产精品入口| 大鸡吧插逼逼视频免费看| 日日操综合成人av| 18禁无翼鸟成人在线| 97国产在线观看高清| 美女骚逼日出水来了| 亚洲Av无码国产综合色区| 91高清成人在线视频| 看一级特黄a大片日本片黑人| 老司机深夜免费福利视频在线观看| 日本免费午夜视频网站| 亚洲成人av在线一区二区| 国产精品久久久久网| 天天射夜夜操综合网| 国产高清精品极品美女| 亚洲欧美日韩视频免费观看| 在线观看亚洲人成免费网址| 4个黑人操素人视频网站精品91| 日韩午夜福利精品试看| 午夜精品在线视频一区| 91chinese在线视频| 亚洲综合另类欧美久久| 亚洲av第国产精品| 天天操天天干天天日狠狠插 | 99久久激情婷婷综合五月天| 1000部国产精品成人观看视频| 91福利视频免费在线观看| 中文字幕日韩无敌亚洲精品| 中文字幕人妻一区二区视频| 干逼又爽又黄又免费的视频| 久久久久久久久久久久久97| 亚洲视频乱码在线观看| 女同互舔一区二区三区| 少妇人妻二三区视频| 91在线免费观看成人| 91精品国产综合久久久蜜| 亚洲一区自拍高清免费视频| v888av在线观看视频| 天天日天天天天天天天天天天| 午夜精品久久久久久99热| 亚洲欧美在线视频第一页| 黄色片黄色片wyaa| 另类av十亚洲av| 亚洲伊人久久精品影院一美女洗澡 | 色综合天天综合网国产成人| 动色av一区二区三区| 日本最新一二三区不卡在线| 国语对白xxxx乱大交| 超碰公开大香蕉97| 成人乱码一区二区三区av| 精品视频中文字幕在线播放| 一区二区三区 自拍偷拍| 九色视频在线观看免费| 中文字幕高清资源站| 人妻最新视频在线免费观看| 久久免费看少妇高潮完整版| 日本成人不卡一区二区| 天天操天天操天天碰| 红杏久久av人妻一区| 欧洲日韩亚洲一区二区三区| 高清一区二区欧美系列| 久草视频首页在线观看| 国产视频一区在线观看| 久久www免费人成一看片| 日韩精品中文字幕福利| 欧美一区二区三区四区性视频| 亚洲av自拍天堂网| 日韩精品中文字幕在线| 在线不卡成人黄色精品| 中文字幕一区二区自拍| 黄片色呦呦视频免费看| 日本少妇人妻xxxxxhd| 日本一区精品视频在线观看| 韩国爱爱视频中文字幕| 精品人妻伦一二三区久| 日本人竟这样玩学生妹| 1000小视频在线| 97国产在线观看高清| 国产成人精品久久二区91| 国产午夜福利av导航| 国产麻豆国语对白露脸剧情| 欧美在线一二三视频| 2020中文字幕在线播放| 日韩激情文学在线视频| 日韩精品二区一区久久| 欧美80老妇人性视频| 黄色资源视频网站日韩| 亚洲成av人无码不卡影片一| 75国产综合在线视频| 日本人妻精品久久久久久| 久久精品美女免费视频| 午夜精品久久久久久99热| 在线观看操大逼视频| 无码日韩人妻精品久久| 熟女人妻一区二区精品视频| 亚洲精品成人网久久久久久小说| 福利视频广场一区二区| www日韩毛片av| 91 亚洲视频在线观看| 98视频精品在线观看| 国产精品手机在线看片| 欧美亚洲免费视频观看| 国产精品久久综合久久| 午夜久久久久久久99| 美女福利视频网址导航| 99精品久久久久久久91蜜桃| 超碰97人人澡人人| 亚洲精品在线资源站| av手机在线免费观看日韩av| 涩涩的视频在线观看视频| 大香蕉大香蕉大香蕉大香蕉大香蕉| 国产亚洲精品欧洲在线观看| 国产自拍黄片在线观看| 国内资源最丰富的网站| 欧美成人一二三在线网| 最新91精品视频在线| 美女张开腿让男生操在线看| 另类av十亚洲av| 成人24小时免费视频| 同居了嫂子在线播高清中文| 男女啪啪视频免费在线观看| 色婷婷精品大在线观看| 国产一级麻豆精品免费| 在线免费91激情四射 | 很黄很污很色的午夜网站在线观看| 精品一区二区三区三区色爱| 国产精品中文av在线播放| 四川乱子伦视频国产vip| 福利视频网久久91| 大胆亚洲av日韩av| 少妇一区二区三区久久久| 伊人网中文字幕在线视频| 中文字幕之无码色多多| av手机在线免费观看日韩av| 都市家庭人妻激情自拍视频| 青青操免费日综合视频观看| 天天通天天透天天插| 一区二区三区四区五区性感视频| 精品黑人巨大在线一区| 天天做天天爽夜夜做少妇| 夜夜骑夜夜操夜夜奸| 青青草亚洲国产精品视频| 九九热99视频在线观看97| 欧美爆乳肉感大码在线观看| 亚洲一区二区激情在线| 国产亚洲精品视频合集| 一级a看免费观看网站| 天天操天天干天天插| 中文亚洲欧美日韩无线码| 一区二区三区在线视频福利| 欧美另类一区二区视频| 精品久久久久久久久久中文蒉| 国产黄色a级三级三级三级 | 免费在线观看视频啪啪| 日韩特级黄片高清在线看| 午夜久久久久久久精品熟女| 国产亚洲精品视频合集| 人妻少妇亚洲精品中文字幕| 18禁无翼鸟成人在线| 在线观看一区二区三级| 天堂av在线官网中文| 男人靠女人的逼视频| 亚洲欧洲av天堂综合| 玖玖一区二区在线观看| 久久免费看少妇高潮完整版| 欧美伊人久久大香线蕉综合| 亚洲一区二区三区uij| 亚洲日本一区二区久久久精品| 久久亚洲天堂中文对白| 欧美亚洲偷拍自拍色图| 免费岛国喷水视频在线观看| av在线播放国产不卡| 中文字幕奴隷色的舞台50| 福利国产视频在线观看| 骚逼被大屌狂草视频免费看| 午夜美女少妇福利视频| 天堂av中文在线最新版| 国产污污污污网站在线| 1区2区3区4区视频在线观看| 涩爱综合久久五月蜜臀| 日本性感美女三级视频| 天天日夜夜干天天操| 国产精品成久久久久三级蜜臀av | 一区二区免费高清黄色视频| 91国产在线免费播放| 亚洲午夜高清在线观看| 天天操天天爽天天干| 中文字幕日韩精品日本| 小穴多水久久精品免费看| 国产女人露脸高潮对白视频| 亚洲熟女综合色一区二区三区四区| 日韩熟女av天堂系列| 2020av天堂网在线观看| 啪啪啪操人视频在线播放| 人妻另类专区欧美制服| 性欧美激情久久久久久久| 日本一道二三区视频久久 | 91九色国产porny蝌蚪| 1区2区3区不卡视频| 姐姐的朋友2在线观看中文字幕| 亚洲综合色在线免费观看| 久久久制服丝袜中文字幕| 国产精品大陆在线2019不卡| av线天堂在线观看| 中文字幕人妻被公上司喝醉在线| 亚洲在线一区二区欧美| 九色porny九色9l自拍视频| 任我爽精品视频在线播放| 午夜在线观看一区视频| 欧美视频不卡一区四区| av中文字幕在线导航| 女蜜桃臀紧身瑜伽裤| 国产中文精品在线观看| 熟女妇女老妇一二三区| 亚洲国产成人在线一区| 97超碰最新免费在线观看| 日美女屁股黄邑视频| 午夜在线一区二区免费| 久草视频在线一区二区三区资源站| 性感美女高潮视频久久久| 黄色无码鸡吧操逼视频| 天堂av在线官网中文| 一区二区三区视频,福利一区二区| 亚洲欧美福利在线观看| 天堂av中文在线最新版| 岛国免费大片在线观看| 日韩精品激情在线观看| 国产伊人免费在线播放| 特级欧美插插插插插bbbbb| 欧美一区二区三区激情啪啪啪| 青青青激情在线观看视频| 欧美viboss性丰满| 一本久久精品一区二区| 99精品国产自在现线观看| 可以在线观看的av中文字幕| 亚洲另类在线免费观看| 久久久久久cao我的性感人妻| 黄色成人在线中文字幕| 一区二区三区四区视频在线播放 | 成人福利视频免费在线| 亚洲成人av一区久久| 国产成人自拍视频在线免费观看| 91久久人澡人人添人人爽乱| 青青操免费日综合视频观看| 美女被肏内射视频网站| 国产福利小视频大全| 久久久久久久久久一区二区三区| 日韩欧美一级精品在线观看| 天天摸天天干天天操科普| 久久久久久久亚洲午夜综合福利| 好太好爽好想要免费| 馒头大胆亚洲一区二区| 日韩剧情片电影在线收看| 青青草原网站在线观看| 免费成人va在线观看| 大鸡巴操b视频在线| 成人精品视频99第一页| 男生用鸡操女生视频动漫| 欧美日韩精品永久免费网址 | 欧美香蕉人妻精品一区二区| 欧美视频中文一区二区三区| 精品国产污污免费网站入口自| 久久丁香花五月天色婷婷| 欧美区一区二区三视频| 欧美视频不卡一区四区| 天天日天天干天天插舔舔| 99精品免费久久久久久久久a| 亚欧在线视频你懂的| 丰满少妇人妻xxxxx| 蝴蝶伊人久久中文娱乐网 | 成年人的在线免费视频| 92福利视频午夜1000看 | 天天躁夜夜躁日日躁a麻豆| 国产亚洲视频在线二区| av天堂资源最新版在线看| 一区二区三区四区视频在线播放| 18禁美女黄网站色大片下载| 1024久久国产精品| 人妻最新视频在线免费观看| 人人妻人人澡人人爽人人dvl| 综合色区亚洲熟妇shxstz| 午夜国产免费福利av| 青青草亚洲国产精品视频| 天天草天天色天天干| 亚洲福利精品视频在线免费观看| 亚洲va国产va欧美精品88| 99一区二区在线观看| 亚洲熟色妇av日韩熟色妇在线| 久草视频 久草视频2| 中文字幕免费福利视频6| 久久美欧人妻少妇一区二区三区| 天堂av狠狠操蜜桃| 亚洲国产精品免费在线观看| 91九色porny国产蝌蚪视频| 久久久久久久久久久免费女人| 任你操任你干精品在线视频| 91九色porny蝌蚪国产成人| 欧美亚洲国产成人免费在线 | 视频一区二区三区高清在线| 欧美伊人久久大香线蕉综合| 熟女人妻一区二区精品视频| 欧美亚洲自偷自拍 在线| 88成人免费av网站| 亚洲高清自偷揄拍自拍| 中文字幕无码一区二区免费| 被大鸡吧操的好舒服视频免费| 国产黑丝高跟鞋视频在线播放| 亚洲高清国产一区二区三区| www日韩毛片av| 日韩欧美高清免费在线| 美女福利写真在线观看视频| 亚洲成人国产综合一区| 黄色片黄色片wyaa| 亚洲久久午夜av一区二区| 亚洲另类图片蜜臀av| 亚洲 自拍 色综合图| 三级等保密码要求条款| 含骚鸡巴玩逼逼视频| 日韩黄色片在线观看网站| 99国内小视频在现欢看| 中国熟女一区二区性xx| 九色porny九色9l自拍视频| 国产视频网站一区二区三区| 国产欧美日韩第三页| 欧美一区二区三区激情啪啪啪| 欧美成人黄片一区二区三区| 偷拍自拍视频图片免费| 天天日天天日天天射天天干| 日本一二三中文字幕| 一区二区在线观看少妇| 国产麻豆国语对白露脸剧情| 亚洲成人激情视频免费观看了| 亚洲最大黄了色网站| 欧美亚洲牲夜夜综合久久| 久久久久国产成人精品亚洲午夜| 日本xx片在线观看| av老司机精品在线观看| 欧亚乱色一区二区三区| 99久久99一区二区三区| av高潮迭起在线观看| 国产在线观看免费人成短视频| 国产又粗又硬又大视频| 日韩熟女av天堂系列| 成熟熟女国产精品一区| 做爰视频毛片下载蜜桃视频1| 老司机99精品视频在线观看 | 午夜国产免费福利av| 亚洲国产在人线放午夜| 后入美女人妻高清在线| 国产一区二区在线欧美| 视频一区 视频二区 视频| 成人免费毛片aaaa| 97少妇精品在线观看| 亚洲1069综合男同| 国产成人无码精品久久久电影| 日本少妇人妻xxxxxhd| 日韩午夜福利精品试看| 色综合久久无码中文字幕波多| 早川濑里奈av黑人番号| caoporn蜜桃视频| yy6080国产在线视频| 亚洲国产欧美一区二区三区久久 | 一级A一级a爰片免费免会员 | 亚洲精品国产久久久久久| 国产福利小视频免费观看| 97超碰人人搞人人| 欧美在线一二三视频| 日本黄在免费看视频| 扒开腿挺进肉嫩小18禁视频| 国产高清在线在线视频| 久久久久久97三级| 亚洲 色图 偷拍 欧美| 3344免费偷拍视频| 色综合久久五月色婷婷综合| 成人av在线资源网站| 91自产国产精品视频| 特大黑人巨大xxxx| 国产伦精品一区二区三区竹菊| 黄色资源视频网站日韩| 啪啪啪18禁一区二区三区| 免费在线看的黄网站| 免费一级特黄特色大片在线观看| 久久精品国产999| 国产va在线观看精品| 国产chinesehd精品麻豆| 天天干夜夜操天天舔| 日本一道二三区视频久久| 中文字幕在线免费第一页| 国产va精品免费观看| aiss午夜免费视频| 中文字幕在线永久免费播放| 中文字幕免费福利视频6| 免费黄色成人午夜在线网站| 亚洲国产第一页在线观看| 97色视频在线观看| 久久精品国产999| 青青擦在线视频国产在线| 激情伦理欧美日韩中文字幕 | 视频久久久久久久人妻| 大香蕉大香蕉大香蕉大香蕉大香蕉| 亚洲一级av大片免费观看| 青青青青青青青青青青草青青 | 国产成人综合一区2区| 国产精品国产三级国产精东 | 大鸡巴操娇小玲珑的女孩逼| 99热国产精品666| AV无码一区二区三区不卡| 久久热久久视频在线观看| 啪啪啪啪啪啪啪啪av| 蜜桃视频在线欧美一区| 韩国AV无码不卡在线播放| 2018在线福利视频| 老师啊太大了啊啊啊尻视频| 馒头大胆亚洲一区二区| 午夜dv内射一区区| 中文字幕人妻av在线观看| 自拍偷拍亚洲另类色图| 最后99天全集在线观看| 国产成人精品av网站| 欧美特级特黄a大片免费| 国产av自拍偷拍盛宴| 欧美天堂av无线av欧美| 中文字幕成人日韩欧美| av森泽佳奈在线观看| 福利午夜视频在线观看| 亚洲精品国品乱码久久久久| 超级av免费观看一区二区三区| 亚洲精品福利网站图片| 中文字幕国产专区欧美激情| 中文字幕免费福利视频6| 欧洲黄页网免费观看| 国语对白xxxx乱大交| 老司机免费视频网站在线看| 一区二区三区四区五区性感视频| 中文字幕免费福利视频6| 免费在线播放a级片| 中文字幕视频一区二区在线观看 | 欧洲精品第一页欧洲精品亚洲 | 骚逼被大屌狂草视频免费看| 视频在线亚洲一区二区| 风流唐伯虎电视剧在线观看 | 国产欧美精品不卡在线| 亚洲av无乱一区二区三区性色| 视频 国产 精品 熟女 | 国产高清在线观看1区2区| 激情人妻校园春色亚洲欧美| 九色精品视频在线播放| 中文字幕一区的人妻欧美日韩| 国产麻豆91在线视频| 狠狠躁狠狠爱网站视频| 国产精品视频欧美一区二区| 亚洲高清免费在线观看视频| 欧美成人精品欧美一级黄色| 风流唐伯虎电视剧在线观看 | 偷拍自拍亚洲美腿丝袜| 99热这里只有国产精品6| 天天色天天爱天天爽| 一区二区麻豆传媒黄片| 国产日韩一区二区在线看| 天天干天天爱天天色| 伊人情人综合成人久久网小说| 欧美日本在线视频一区| 国产亚洲精品欧洲在线观看| 日韩欧美一级精品在线观看| 在线新三级黄伊人网| 天天日天天日天天射天天干| 日本又色又爽又黄又粗| 99久久久无码国产精品性出奶水 | 精品久久久久久久久久久久人妻| 国产在线免费观看成人| 亚洲色偷偷综合亚洲AV伊人| 在线观看日韩激情视频| asmr福利视频在线观看| 午夜久久香蕉电影网| av黄色成人在线观看| 久久免看30视频口爆视频| 午夜频道成人在线91| 成年午夜影片国产片| 搞黄色在线免费观看| 国产亚洲视频在线观看| 六月婷婷激情一区二区三区| 人妻3p真实偷拍一二区| 青青青艹视频在线观看| 78色精品一区二区三区| 福利国产视频在线观看| 久久久人妻一区二区| 91www一区二区三区| 在线视频这里只有精品自拍| 99的爱精品免费视频| 瑟瑟视频在线观看免费视频| 天天日天天日天天擦| 人妻最新视频在线免费观看| 91免费观看在线网站| 男生用鸡操女生视频动漫| 日本三极片中文字幕| 国产高潮无码喷水AV片在线观看| 黑人变态深video特大巨大| 特级欧美插插插插插bbbbb| 亚洲综合一区成人在线| 久久久久久97三级| 男人靠女人的逼视频| 日本精品美女在线观看| 色在线观看视频免费的| 精品日产卡一卡二卡国色天香| www日韩毛片av| 9久在线视频只有精品| 福利片区一区二体验区| 99热久久这里只有精品8| 欧美一区二区三区乱码在线播放 | 久久久精品999精品日本| 一区二区三区四区视频| 免费高清自慰一区二区三区网站| 欧美精品伦理三区四区| 白白操白白色在线免费视频| 夏目彩春在线中文字幕| 男女啪啪啪啪啪的网站| 欧美性感尤物人妻在线免费看| 日本xx片在线观看| 91九色porny蝌蚪国产成人| av森泽佳奈在线观看| 欧美韩国日本国产亚洲| 青青社区2国产视频| 欧美精品激情在线最新观看视频| 亚洲精品欧美日韩在线播放| 欧美专区第八页一区在线播放| 熟女人妻在线中出观看完整版| 大香蕉福利在线观看| 2022中文字幕在线| 亚洲精品精品国产综合| 福利在线视频网址导航| 北条麻妃肉色丝袜视频| 欧美视频中文一区二区三区| 一区二区免费高清黄色视频| 国产日韩一区二区在线看| 日日夜夜狠狠干视频| 国产精品成人xxxx| 红桃av成人在线观看| av中文字幕在线观看第三页| 一区二区三区麻豆福利视频| 中字幕人妻熟女人妻a62v网| 女同性ⅹxx女同h偷拍| 亚洲成人激情视频免费观看了| 激情人妻校园春色亚洲欧美 | 18禁美女无遮挡免费| 日韩欧美中文国产在线 | 中文字幕在线第一页成人| 六月婷婷激情一区二区三区| 99热这里只有国产精品6| 大肉大捧一进一出好爽在线视频| 无码国产精品一区二区高潮久久4| 51国产成人精品视频| 中文字幕 码 在线视频| 成人免费毛片aaaa| 岛国黄色大片在线观看| 中文字幕一区二区亚洲一区| 少妇高潮无套内谢麻豆| 欧美成人小视频在线免费看| 97人妻色免费视频| 欧美爆乳肉感大码在线观看| 欧美黑人性猛交xxxxⅹooo| 国产aⅴ一线在线观看| 日本啪啪啪啪啪啪啪| 日韩av有码一区二区三区4| 制丝袜业一区二区三区| 亚洲av成人免费网站| 97国产在线av精品| 免费观看成年人视频在线观看| 天天爽夜夜爽人人爽QC| 综合精品久久久久97| 欧美精品 日韩国产| 日本黄在免费看视频| 97精品成人一区二区三区| 人妻无码中文字幕专区| 熟女视频一区,二区,三区| 国产性感美女福利视频| 国产激情av网站在线观看| 日韩美女精品视频在线观看网站| 国产av福利网址大全| 男生舔女生逼逼的视频| 亚洲av色香蕉一区二区三区| 班长撕开乳罩揉我胸好爽| 亚洲 欧美 精品 激情 偷拍| 亚洲精品av在线观看| 天天操天天弄天天射| 国产精品一区二区三区蜜臀av| 姐姐的朋友2在线观看中文字幕| 国产精品一区二区三区蜜臀av| 国产一区二区神马久久| 国产va精品免费观看| 精品91高清在线观看| 真实国模和老外性视频| 国产伊人免费在线播放| 少妇被强干到高潮视频在线观看| 日本a级视频老女人| 欧美老鸡巴日小嫩逼| 69精品视频一区二区在线观看| 中文字幕在线免费第一页| av在线观看网址av| 人妻丝袜av在线播放网址| 中文字幕AV在线免费看 | 福利午夜视频在线观看| 免费高清自慰一区二区三区网站| 黄色片一级美女黄色片| 国产一区成人在线观看视频 | 亚洲另类伦春色综合小| 熟女在线视频一区二区三区| 欧美麻豆av在线播放| 国产不卡av在线免费| av新中文天堂在线网址| 国产精品一区二区av国| 亚洲最大黄 嗯色 操 啊| 丝袜长腿第一页在线| aⅴ五十路av熟女中出| 国产欧美日韩第三页| 天堂女人av一区二区| 亚洲天堂成人在线观看视频网站| 3344免费偷拍视频| 一区二区三区久久中文字幕| 瑟瑟视频在线观看免费视频| av老司机亚洲一区二区| 成人性爱在线看四区| 亚洲成人av在线一区二区| 国产视频网站一区二区三区| 免费黄高清无码国产| 国产91精品拍在线观看| 偷拍美女一区二区三区| 亚洲av人人澡人人爽人人爱 | 家庭女教师中文字幕在线播放| 成年人中文字幕在线观看| 精品美女在线观看视频在线观看| 99婷婷在线观看视频| 亚洲精品成人网久久久久久小说| 天天日天天敢天天干| 美女福利视频网址导航| 亚洲精品三级av在线免费观看| 狠狠躁夜夜躁人人爽天天天天97| 国产综合视频在线看片| 视频在线亚洲一区二区| www天堂在线久久| 国产亚州色婷婷久久99精品| 在线观看的a站 最新| 五月婷婷在线观看视频免费| 国产高清女主播在线| 91精品激情五月婷婷在线| 亚洲精品无码色午夜福利理论片| av日韩在线观看大全| 黄色片一级美女黄色片| 女同性ⅹxx女同h偷拍| 99久久99久国产黄毛片| 国产精品久久久久网| 天堂v男人视频在线观看| 啊啊好大好爽啊啊操我啊啊视频| 国产中文精品在线观看| 高清成人av一区三区| 夜夜操,天天操,狠狠操| 亚洲在线一区二区欧美| 九色精品视频在线播放| 亚洲中文字幕综合小综合| 香蕉aⅴ一区二区三区| 人妻凌辱欧美丰满熟妇| 中文字幕日韩精品日本| 久草免费人妻视频在线| 亚洲欧美国产综合777| 一区二区三区视频,福利一区二区| 精品av国产一区二区三区四区| 99久久99一区二区三区| 欧美乱妇无乱码一区二区| 久久精品在线观看一区二区| 天天日天天天天天天天天天天| 日韩美女综合中文字幕pp| 91p0rny九色露脸熟女| 日本丰满熟妇BBXBBXHD| 久久综合老鸭窝色综合久久| 国产老熟女伦老熟妇ⅹ| 中文字幕一区二 区二三区四区| 五月天色婷婷在线观看视频免费| 日本av在线一区二区三区| 国产实拍勾搭女技师av在线| 福利午夜视频在线合集| 三级等保密码要求条款| 久久人人做人人妻人人玩精品vr | 伊人网中文字幕在线视频| 国产精品手机在线看片| 福利视频一区二区三区筱慧| 99热这里只有精品中文| 免费黄页网站4188| 视频在线亚洲一区二区| 99久久中文字幕一本人| 亚洲黄色av网站免费播放| 国产乱子伦一二三区| 美女张开两腿让男人桶av| 日韩黄色片在线观看网站| 在线观看视频网站麻豆| 免费在线黄色观看网站| 黄色成年网站午夜在线观看| 好了av中文字幕在线| 国产精品国产三级国产午| 久久久精品欧洲亚洲av| 日本啪啪啪啪啪啪啪| 国产精品女邻居小骚货| 中文字幕在线视频一区二区三区| 国产午夜亚洲精品麻豆| 骚货自慰被发现爆操| 青青青青青青青青青青草青青 | 精品成人啪啪18免费蜜臀| 人人人妻人人澡人人| 久久精品美女免费视频| 天天做天天干天天操天天射| 亚洲欧美福利在线观看| 日本www中文字幕| 制丝袜业一区二区三区| 欧美日本国产自视大全| 日韩一区二区三区三州| 一区二区三区的久久的蜜桃的视频| 亚洲粉嫩av一区二区三区| 国产精品国产三级国产午| 国产精品免费不卡av| 粉嫩av懂色av蜜臀av| 久草视频在线一区二区三区资源站 | 日本18禁久久久久久| 午夜影院在线观看视频羞羞羞| 亚洲av成人网在线观看| 亚洲1区2区3区精华液| 亚洲欧美清纯唯美另类| 欧美另类z0z变态| 久久永久免费精品人妻专区| 欧美视频中文一区二区三区| 欧美爆乳肉感大码在线观看| 91中文字幕最新合集| 青青青aaaa免费| av中文字幕在线观看第三页| 男大肉棒猛烈插女免费视频| v888av在线观看视频| 中文字幕第一页国产在线| 自拍偷区二区三区麻豆| 国产成人综合一区2区| www,久久久,com| 91社福利《在线观看| 美日韩在线视频免费看| 欧美特色aaa大片| 亚洲熟色妇av日韩熟色妇在线| 777奇米久久精品一区| av俺也去在线播放| 中英文字幕av一区| 国产午夜亚洲精品不卡在线观看| 91破解版永久免费| 久草视频在线一区二区三区资源站 | 亚洲高清一区二区三区视频在线| 一区二区三区的久久的蜜桃的视频 | 亚洲国产第一页在线观看| 日本性感美女视频网站| 一区二区三区日韩久久| 日本精品美女在线观看| 欧美精品国产综合久久| 黄色av网站免费在线| 青青青青青青青青青青草青青| 极品粉嫩小泬白浆20p主播| 欧美精品激情在线最新观看视频| 精品黑人一区二区三区久久国产 | 国产精品成久久久久三级蜜臀av| 久碰精品少妇中文字幕av| 2020国产在线不卡视频 | 精品一区二区三区三区色爱| 91she九色精品国产| 中文字幕在线第一页成人| 一区二区三区四区视频| 中文字幕在线欧美精品| 亚洲一区二区久久久人妻| 精品欧美一区二区vr在线观看| 黄片大全在线观看观看| 亚洲精品麻豆免费在线观看| 美日韩在线视频免费看| 被大鸡吧操的好舒服视频免费| 超级福利视频在线观看| 激情图片日韩欧美人妻| 日韩欧美在线观看不卡一区二区| 亚洲精品av在线观看| avjpm亚洲伊人久久| 亚洲第一黄色在线观看| 男人在床上插女人视频| 国产成人精品久久二区91| 亚洲成人国产av在线| 在线播放国产黄色av| xxx日本hd高清| 欧美综合婷婷欧美综合| 巨乳人妻日下部加奈被邻居中出| 日韩成人免费电影二区| tube69日本少妇| 51国产成人精品视频| av资源中文字幕在线观看| 2022国产精品视频| 动漫黑丝美女的鸡巴| 自拍 日韩 欧美激情| 18禁网站一区二区三区四区 | 国产精品自拍偷拍a| 大屁股熟女一区二区三区| 一区二区三区欧美日韩高清播放| 亚洲卡1卡2卡三卡四老狼| 日本三极片中文字幕| 在线观看国产网站资源| 99久久99一区二区三区| 国产视频网站一区二区三区| 亚洲人成精品久久久久久久| aaa久久久久久久久| 啪啪啪操人视频在线播放| 特级无码毛片免费视频播放 | 好吊视频—区二区三区| 免费观看成年人视频在线观看| 国产 在线 免费 精品| 久久午夜夜伦痒痒想咳嗽P| 日本一二三中文字幕| 黄色大片男人操女人逼| 二区中出在线观看老师| 日韩av大胆在线观看| 天堂v男人视频在线观看| 亚洲麻豆一区二区三区| 欧美性感尤物人妻在线免费看| 人人妻人人澡人人爽人人dvl| 国产91久久精品一区二区字幕| 天天操夜夜操天天操天天操| 狠狠嗨日韩综合久久| 国产精品久久9999| 同居了嫂子在线播高清中文| 午夜激情高清在线观看| 国产九色91在线视频| 日本福利午夜电影在线观看| 中文字幕成人日韩欧美| 天天干天天插天天谢| 老熟妇xxxhd老熟女| 国际av大片在线免费观看| 最新国产精品网址在线观看| 成年女人免费播放视频| 天天操天天操天天碰| 欧美精品国产综合久久| 人人爽亚洲av人人爽av| 亚洲图库另类图片区| 色婷婷久久久久swag精品| 99热久久这里只有精品8| 在线观看视频污一区| 老师让我插进去69AV| 五月天中文字幕内射| 亚洲国产在人线放午夜| 新婚人妻聚会被中出| 亚洲国产成人无码麻豆艾秋| 国产午夜男女爽爽爽爽爽视频| 欧美在线精品一区二区三区视频| 日比视频老公慢点好舒服啊| 天天日天天操天天摸天天舔| 精品91高清在线观看| 免费男阳茎伸入女阳道视频| 天天摸天天亲天天舔天天操天天爽 | 日本午夜福利免费视频| 绯色av蜜臀vs少妇| 亚洲另类在线免费观看| 日本午夜福利免费视频| 精品91自产拍在线观看一区| 天天做天天干天天操天天射| 亚洲欧美国产综合777| 午夜精品一区二区三区城中村| 欧美日本aⅴ免费视频| 欧美成人精品在线观看| 极品性荡少妇一区二区色欲| 性色av一区二区三区久久久| 国产成人综合一区2区| 91久久综合男人天堂| 国内精品在线播放第一页| 国产伊人免费在线播放| 五十路熟女av天堂| 亚洲变态另类色图天堂网| 狠狠躁狠狠爱网站视频| 国产第一美女一区二区三区四区| 精品久久久久久久久久久99| 97人妻无码AV碰碰视频| 又大又湿又爽又紧A视频| 男生舔女生逼逼视频| 欧洲国产成人精品91铁牛tv| 亚洲日产av一区二区在线| 亚洲图片欧美校园春色| 亚洲中文精品字幕在线观看| 性色av一区二区三区久久久| 国产高潮无码喷水AV片在线观看| 可以免费看的www视频你懂的| 好了av中文字幕在线| 日韩成人综艺在线播放| 成人免费公开视频无毒| 午夜婷婷在线观看视频| 亚洲 图片 欧美 图片| 人妻自拍视频中国大陆| 欧美一级色视频美日韩| 中文字幕在线一区精品| 青青青国产片免费观看视频| 亚洲卡1卡2卡三卡四老狼| 国产精彩福利精品视频| 99av国产精品欲麻豆| 99热碰碰热精品a中文| 男大肉棒猛烈插女免费视频| 欧美日韩亚洲国产无线码| 久久热这里这里只有精品| 国产日韩欧美视频在线导航|