国产无遮挡裸体免费直播视频,久久精品国产蜜臀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

最新評論

大鸡巴操娇小玲珑的女孩逼| 久久精品36亚洲精品束缚| 粉嫩欧美美人妻小视频| 国产亚州色婷婷久久99精品| 欧美 亚洲 另类综合| 青青热久免费精品视频在线观看| 免费男阳茎伸入女阳道视频 | 亚洲精品色在线观看视频| 三级等保密码要求条款| 国产午夜亚洲精品麻豆| 亚洲日本一区二区三区| 视频 一区二区在线观看| 国产又粗又猛又爽又黄的视频美国| 一级黄色片夫妻性生活| 亚洲在线一区二区欧美| 久久精品在线观看一区二区| 国产av国片精品一区二区| 欧美精品国产综合久久| 亚洲欧美另类手机在线| 日韩加勒比东京热二区| 美女被肏内射视频网站| 欧美综合婷婷欧美综合| 久草视频在线一区二区三区资源站| av老司机亚洲一区二区| 99精品国产免费久久| 亚洲av无女神免非久久| 丰满熟女午夜福利视频| 午夜精品一区二区三区城中村| 在线不卡成人黄色精品| 亚洲中文精品人人免费| 中文字幕 人妻精品| 99精品亚洲av无码国产另类| 9色在线视频免费观看| 欧美国产亚洲中英文字幕| 把腿张开让我插进去视频| 中文字幕熟女人妻久久久| 日韩影片一区二区三区不卡免费| 做爰视频毛片下载蜜桃视频1 | 视频啪啪啪免费观看| 在线视频精品你懂的| 亚洲精品ww久久久久久| 婷婷五月亚洲综合在线| 91快播视频在线观看| 宅男噜噜噜666免费观看| 99精品国产免费久久| 日韩a级精品一区二区| av在线观看网址av| 在线 中文字幕 一区| 亚洲高清国产自产av| 成年人啪啪视频在线观看| 91免费观看国产免费| 九九视频在线精品播放| 极品丝袜一区二区三区| 偷拍自拍国产在线视频| 香港一级特黄大片在线播放| 中国无遮挡白丝袜二区精品 | 国产性色生活片毛片春晓精品 | 夜夜骑夜夜操夜夜奸| 亚洲成人情色电影在线观看| 国产高清在线在线视频| 久久热久久视频在线观看| 色哟哟在线网站入口| 综合一区二区三区蜜臀| 年轻的人妻被夫上司侵犯| 国产自拍在线观看成人| 男生舔女生逼逼视频| 夜夜操,天天操,狠狠操| 免费观看污视频网站| 天天日天天舔天天射进去| 中文字幕人妻av在线观看| 美女日逼视频免费观看| 国产视频在线视频播放| 自拍偷拍一区二区三区图片| 99re国产在线精品| 亚洲成av人无码不卡影片一| 国产成人午夜精品福利| 亚洲国产精品久久久久蜜桃| 国产综合视频在线看片| 天天日天天干天天舔天天射| 97人妻无码AV碰碰视频| 日韩成人免费电影二区| 中文字幕日韩91人妻在线| 日韩特级黄片高清在线看| 天天干天天插天天谢| 国产卡一卡二卡三乱码手机| 人人妻人人澡欧美91精品| 美女骚逼日出水来了| 亚洲av男人的天堂你懂的| 日曰摸日日碰夜夜爽歪歪| 亚洲人妻视频在线网| 亚洲天堂有码中文字幕视频| 中文字幕奴隷色的舞台50| 青青草亚洲国产精品视频| 国产白袜脚足J棉袜在线观看| 香蕉片在线观看av| 好吊操视频这里只有精品| 后入美女人妻高清在线| 2018最新中文字幕在线观看| 人人妻人人爽人人添夜| 11久久久久久久久久久| 成人福利视频免费在线| 成人免费毛片aaaa| 男人操女人逼逼视频网站| 日韩精品电影亚洲一区| 女同性ⅹxx女同hd| 激情色图一区二区三区| 亚洲人妻视频在线网| 99人妻视频免费在线| 欧美精产国品一二三产品区别大吗| 影音先锋女人av噜噜色| 人妻激情图片视频小说| 少妇被强干到高潮视频在线观看| 亚洲另类综合一区小说| 11久久久久久久久久久| 97国产在线av精品| 蜜桃臀av蜜桃臀av| 国产综合精品久久久久蜜臀| 在线视频自拍第三页| 国产男女视频在线播放| 国产一区二区火爆视频 | 最新欧美一二三视频 | 成人午夜电影在线观看 久久| 中文字幕在线永久免费播放| 精品亚洲在线免费观看| 人妻少妇性色欲欧美日韩| 日本av在线一区二区三区| 999九九久久久精品| 久草视频中文字幕在线观看| 成人乱码一区二区三区av| 九色视频在线观看免费| 66久久久久久久久久久| 2020av天堂网在线观看| 被大鸡吧操的好舒服视频免费| 美女骚逼日出水来了| 日视频免费在线观看| 精品美女久久久久久| 狍和女人的王色毛片| 亚洲青青操骚货在线视频| av高潮迭起在线观看| 加勒比视频在线免费观看| 3337p日本欧洲大胆色噜噜| 色噜噜噜噜18禁止观看| 1024久久国产精品| 国产不卡av在线免费| 超碰97人人澡人人| 日韩精品中文字幕在线| 天天色天天操天天透| 国产精品黄色的av| 激情综合治理六月婷婷| 国产极品精品免费视频| 五十路熟女av天堂| 亚洲午夜伦理视频在线| 国产chinesehd精品麻豆| 天天干天天爱天天色| 97欧洲一区二区精品免费| 区一区二区三国产中文字幕| 色综合色综合色综合色| 欧美少妇性一区二区三区| 亚洲欧洲av天堂综合| 亚洲国际青青操综合网站| av久久精品北条麻妃av观看| 欧美精品亚洲精品日韩在线| 伊人日日日草夜夜草| 久久精品国产亚洲精品166m| 国产精品久久久久久美女校花| 精品区一区二区三区四区人妻| 欧美亚洲少妇福利视频| 欧美色婷婷综合在线| 国产视频精品资源网站| 91天堂天天日天天操| 成年人该看的视频黄免费| 97超碰国语国产97超碰| 国产精品午夜国产小视频| 日韩亚国产欧美三级涩爱| 免费观看丰满少妇做受| 亚洲熟妇x久久av久久| 黄色成年网站午夜在线观看| 黑人解禁人妻叶爱071| 经典亚洲伊人第一页| 亚洲va欧美va人人爽3p| 福利国产视频在线观看| 在线观看日韩激情视频| 久久精品在线观看一区二区| 熟女俱乐部一二三区| 人妻无码中文字幕专区| 成人在线欧美日韩国产| 亚洲 中文 自拍 另类 欧美| 三上悠亚和黑人665番号| 欧美亚洲少妇福利视频| 狠狠操操操操操操操操操| 亚洲精品久久视频婷婷| 亚洲国产第一页在线观看| 亚洲另类伦春色综合小| 国产午夜福利av导航| 久久久久国产成人精品亚洲午夜| 男人天堂最新地址av| 快点插进来操我逼啊视频| 在线国产日韩欧美视频| 国产丰满熟女成人视频| 宅男噜噜噜666国产| 57pao国产一区二区| 亚洲激情偷拍一区二区| 成人性黑人一级av| 亚洲男人让女人爽的视频| 夫妻在线观看视频91| 在线 中文字幕 一区| 啊啊好慢点插舔我逼啊啊啊视频 | 成人国产小视频在线观看| 大陆av手机在线观看| 日本中文字幕一二区视频| 香蕉aⅴ一区二区三区| 成人免费毛片aaaa| 少妇人妻100系列| 色97视频在线播放| 青春草视频在线免费播放| 国产亚州色婷婷久久99精品| 国产女人被做到高潮免费视频| 午夜久久久久久久99| 国产午夜亚洲精品不卡在线观看| 日本脱亚入欧是指什么| 91片黄在线观看喷潮| 精品一线二线三线日本| 日本精品美女在线观看| 成年人该看的视频黄免费| 天堂av狠狠操蜜桃| 亚洲的电影一区二区三区| 久久www免费人成一看片| 亚洲av一妻不如妾| av在线免费中文字幕| 亚洲狠狠婷婷综合久久app| 特级欧美插插插插插bbbbb| 成人激情文学网人妻| 中文字幕,亚洲人妻| 欧美视频一区免费在线| 国产综合高清在线观看| av视网站在线观看| 黑人巨大的吊bdsm| 亚洲精品久久综合久| 国产麻豆精品人妻av| 黑人进入丰满少妇视频| 欧美另类重口味极品在线观看| 精品欧美一区二区vr在线观看| 孕妇奶水仑乱A级毛片免费看| 日韩美女搞黄视频免费| 国产露脸对白在线观看| 欧美日本aⅴ免费视频| 国产高清女主播在线| 青青草在观免费国产精品| 岛国青草视频在线观看| 午夜极品美女福利视频| 一区二区三区四区五区性感视频| 日韩欧美制服诱惑一区在线| 亚洲av天堂在线播放| 日本一道二三区视频久久 | 亚洲免费va在线播放| 亚洲成人免费看电影| 国产性生活中老年人视频网站| 亚洲国际青青操综合网站| 国产一区二区三免费视频| 日韩精品激情在线观看| 成人激情文学网人妻| 亚洲精品无码久久久久不卡| 国产精品黄片免费在线观看| 91色老99久久九九爱精品| 免费观看理论片完整版| 亚洲午夜精品小视频| 亚洲欧美国产综合777| 国产精品久久久久网| 色哟哟国产精品入口| 成人高潮aa毛片免费| 91精品国产91青青碰| japanese五十路熟女熟妇| 啪啪啪啪啪啪啪啪av| 99久久中文字幕一本人| 婷婷久久久综合中文字幕| 日韩美女综合中文字幕pp| 99热碰碰热精品a中文| 在线免费视频 自拍| 一区二区三区四区视频在线播放| 岛国青草视频在线观看| 制服丝袜在线人妻中文字幕| 亚洲精品乱码久久久久久密桃明| 懂色av之国产精品| 国产91久久精品一区二区字幕| 黄色视频在线观看高清无码| 中文字幕在线视频一区二区三区 | 传媒在线播放国产精品一区| av日韩在线观看大全| 农村胖女人操逼视频| 我想看操逼黄色大片| 白白操白白色在线免费视频 | 中文字幕日韩精品就在这里| 香港一级特黄大片在线播放| 99国内精品永久免费视频| 天天做天天干天天操天天射| 人妻在线精品录音叫床| 国内精品在线播放第一页| 欧美精产国品一二三产品价格| 白白操白白色在线免费视频 | 欧美日本国产自视大全| 亚洲激情偷拍一区二区| 91大神福利视频网| 黄色av网站免费在线| 欧美天堂av无线av欧美| av一本二本在线观看| 大鸡吧插逼逼视频免费看| 在线观看亚洲人成免费网址| 视频在线免费观看你懂得| 小泽玛利亚视频在线观看| av在线资源中文字幕| 东游记中文字幕版哪里可以看到| 日比视频老公慢点好舒服啊| 欧美激情精品在线观看| 免费黄色成人午夜在线网站| 2021年国产精品自拍| 自拍偷拍亚洲精品第2页| 99国内小视频在现欢看| 老司机午夜精品视频资源| 成年午夜影片国产片| 亚洲图片欧美校园春色| 美女视频福利免费看| 偷拍自拍亚洲美腿丝袜| 欧美一级视频一区二区| 色呦呦视频在线观看视频| 2018最新中文字幕在线观看 | 欧美久久久久久三级网| 神马午夜在线观看视频| 一色桃子人妻一区二区三区| 超黄超污网站在线观看| 亚洲乱码中文字幕在线| 亚洲人妻30pwc| 欧美精品亚洲精品日韩在线| 亚洲特黄aaaa片| 久久精品国产亚洲精品166m| 久久久久久九九99精品| 少妇与子乱在线观看| 中文字幕亚洲中文字幕| aⅴ精产国品一二三产品| 521精品视频在线观看| 亚洲欧洲av天堂综合| 嫩草aⅴ一区二区三区| 又大又湿又爽又紧A视频| 日韩美女搞黄视频免费| 日本中文字幕一二区视频| 国产免费高清视频视频| 福利午夜视频在线合集| 亚洲1069综合男同| 激情色图一区二区三区| av中文字幕在线观看第三页| 美味人妻2在线播放| 中文字幕高清免费在线人妻| jiujiure精品视频在线| 国产精品sm调教视频| 国产之丝袜脚在线一区二区三区| 国产实拍勾搭女技师av在线| 国产美女一区在线观看| 国产污污污污网站在线| 成人24小时免费视频| 国产成人无码精品久久久电影| 天天色天天舔天天射天天爽| 国产在线一区二区三区麻酥酥 | 久久精品国产999| a v欧美一区=区三区| 亚洲av黄色在线网站| 少妇高潮无套内谢麻豆| 亚洲伊人久久精品影院一美女洗澡| 热思思国产99re| 色婷婷综合激情五月免费观看 | 大香蕉大香蕉大香蕉大香蕉大香蕉| 亚洲人妻30pwc| 国产大鸡巴大鸡巴操小骚逼小骚逼| 国产午夜激情福利小视频在线| 2021年国产精品自拍| 女生被男生插的视频网站| 中文字幕熟女人妻久久久| 中文字幕在线乱码一区二区| 一个色综合男人天堂| 日本熟女精品一区二区三区| 粉嫩小穴流水视频在线观看| 国产亚洲成人免费在线观看| 亚洲青青操骚货在线视频| 亚洲成高清a人片在线观看| 国产刺激激情美女网站| 少妇ww搡性bbb91| 亚洲精品麻豆免费在线观看 | 老司机深夜免费福利视频在线观看| 欧美性受xx黑人性猛交| 11久久久久久久久久久| 欧美va不卡视频在线观看| 天堂av在线官网中文| 噜噜色噜噜噜久色超碰| 国产成人精品福利短视频| 性欧美日本大妈母与子| 97精品视频在线观看| 91破解版永久免费| 一区二区三区av高清免费| 在线观看视频污一区| 91www一区二区三区| 亚洲视频在线观看高清| 日视频免费在线观看| 国产又粗又猛又爽又黄的视频在线| 丝袜国产专区在线观看| 91精品国产黑色丝袜| 日韩近亲视频在线观看| 天天操,天天干,天天射| 女生自摸在线观看一区二区三区| 99婷婷在线观看视频| 边摸边做超爽毛片18禁色戒 | 三级等保密码要求条款| 青青擦在线视频国产在线| 国产精彩对白一区二区三区| 91破解版永久免费| 男生用鸡操女生视频动漫| 亚洲丝袜老师诱惑在线观看| 欧美麻豆av在线播放| 超碰在线中文字幕一区二区| 青青青青爽手机在线| 亚洲免费福利一区二区三区| 日本少妇高清视频xxxxx| 亚洲变态另类色图天堂网| 美洲精品一二三产区区别| 久久免看30视频口爆视频| 国产美女精品福利在线| 国产精品入口麻豆啊啊啊| 偷拍自拍国产在线视频| 久久久91蜜桃精品ad| 水蜜桃国产一区二区三区| 国产V亚洲V天堂无码欠欠| 精品久久久久久久久久久99| 国产密臀av一区二区三| 啊啊好慢点插舔我逼啊啊啊视频| 免费在线福利小视频| 国产在线91观看免费观看| 第一福利视频在线观看| 丝袜美腿欧美另类 中文字幕| 亚洲精品av在线观看| 亚洲一区二区三区五区| 人妻丝袜诱惑我操她视频| gay gay男男瑟瑟在线网站| 精品国产污污免费网站入口自 | 中文字幕第1页av一天堂网| 国产一区二区在线欧美| 国语对白xxxx乱大交| 女生自摸在线观看一区二区三区| 综合激情网激情五月五月婷婷| 青青青青视频在线播放| 顶级尤物粉嫩小尤物网站| av老司机亚洲一区二区| 91国内视频在线观看| 欧美综合婷婷欧美综合| 57pao国产一区二区| 日韩三级电影华丽的外出| 91av中文视频在线| 精品美女福利在线观看| 久久人人做人人妻人人玩精品vr| 精品一区二区三区午夜| 又色又爽又黄的美女裸体| 男人靠女人的逼视频| av网址在线播放大全| 国产乱子伦一二三区| 天天摸天天干天天操科普| 亚洲熟色妇av日韩熟色妇在线| 精品91自产拍在线观看一区| 在线观看av观看av| 天天插天天色天天日| 成人精品在线观看视频| 伊人综合aⅴ在线网| 97人人妻人人澡人人爽人人精品| 亚洲1卡2卡三卡4卡在线观看| 精品首页在线观看视频| 女警官打开双腿沦为性奴| 97超碰人人搞人人| 成人30分钟免费视频| 日韩精品中文字幕福利| 亚洲av日韩高清hd| 中文字幕在线观看国产片| 色天天天天射天天舔| 中文亚洲欧美日韩无线码| 可以免费看的www视频你懂的| 精彩视频99免费在线| 天天草天天色天天干| 黄色录像鸡巴插进去| 成人蜜桃美臀九一一区二区三区| 丝袜肉丝一区二区三区四区在线 | 亚洲福利午夜久久久精品电影网| 搞黄色在线免费观看| 人人人妻人人澡人人| 亚洲美女自偷自拍11页| 国产卡一卡二卡三乱码手机| 欧美精品久久久久久影院| 欧美精产国品一二三产品价格| 欧美韩国日本国产亚洲| 国产91精品拍在线观看| 2020久久躁狠狠躁夜夜躁 | 一级a看免费观看网站| 亚洲熟妇x久久av久久| 成人高潮aa毛片免费| 亚洲av色香蕉一区二区三区 | 亚洲国产欧美国产综合在线| 天天日天天鲁天天操| 日本又色又爽又黄又粗| 免费十精品十国产网站| 2021年国产精品自拍| 亚洲精品 欧美日韩| 免费大片在线观看视频网站| aⅴ精产国品一二三产品| 亚洲丝袜老师诱惑在线观看| 五十路av熟女松本翔子| 日本乱人一区二区三区| 国产黄色大片在线免费播放| 极品丝袜一区二区三区| 青春草视频在线免费播放| 在线不卡日韩视频播放| 六月婷婷激情一区二区三区| 超pen在线观看视频公开97 | 果冻传媒av一区二区三区| 日韩美av高清在线| 中文字幕免费福利视频6| 欧美成人综合色在线噜噜| 九九视频在线精品播放| 亚洲av无硬久久精品蜜桃| 最新国产精品网址在线观看| 国产亚洲成人免费在线观看| 最新黄色av网站在线观看| 日韩伦理短片在线观看| 啊慢点鸡巴太大了啊舒服视频| 亚洲欧美激情人妻偷拍| 在线国产中文字幕视频| 男生用鸡操女生视频动漫| av日韩在线观看大全| 午夜在线观看岛国av,com| 青青青青爽手机在线| 91在线视频在线精品3| 57pao国产一区二区| 国产福利小视频二区| 欧美色呦呦最新网址| 1区2区3区4区视频在线观看| 免费在线观看视频啪啪| 国产又粗又猛又爽又黄的视频在线| 好吊操视频这里只有精品| 亚洲一区二区三区五区| 在线不卡成人黄色精品| 91久久国产成人免费网站| 97国产福利小视频合集| 啊慢点鸡巴太大了啊舒服视频| 亚洲国际青青操综合网站| 97人人妻人人澡人人爽人人精品| 91国内精品自线在拍白富美| 熟女在线视频一区二区三区| 青青草国内在线视频精选| 欧美视频不卡一区四区| 骚货自慰被发现爆操| 粉嫩小穴流水视频在线观看| 国产亚洲视频在线二区| 青青草国内在线视频精选| 国产乱子伦精品视频潮优女| 自拍 日韩 欧美激情| 99av国产精品欲麻豆| 一区二区三区久久久91| aⅴ五十路av熟女中出| 成年人午夜黄片视频资源| 丝袜国产专区在线观看| 亚洲综合在线视频可播放| 久草免费人妻视频在线| 99视频精品全部15| 另类av十亚洲av| 黄片色呦呦视频免费看| 丁香花免费在线观看中文字幕| 国产麻豆乱子伦午夜视频观看| 人妻少妇av在线观看| 亚洲1卡2卡三卡4卡在线观看| 91精品国产麻豆国产| 免费高清自慰一区二区三区网站| 在线观看911精品国产| 性感美女诱惑福利视频| av日韩在线观看大全| 国产精品久久久黄网站| 大黑人性xxxxbbbb| 国产精品久久久久久美女校花| 色综合久久无码中文字幕波多| 97黄网站在线观看| 日本乱人一区二区三区| 黄页网视频在线免费观看| 一区二区三区蜜臀在线| 亚洲另类综合一区小说| 香港三日本三韩国三欧美三级| 在线观看av2025| 红桃av成人在线观看| 天天射夜夜操狠狠干| 99久久中文字幕一本人| 国产一区二区视频观看| av中文字幕电影在线看| 骚货自慰被发现爆操| 国产精品视频资源在线播放| 久久这里只有精品热视频 | 亚洲欧美国产综合777| 亚洲乱码中文字幕在线| 大尺度激情四射网站| 天天做天天干天天舔| 美日韩在线视频免费看| 人妻在线精品录音叫床| 欧美viboss性丰满| 天天爽夜夜爽人人爽QC| 亚洲熟妇久久无码精品| 色综合久久无码中文字幕波多| 99热99这里精品6国产| 精品成人午夜免费看| 女警官打开双腿沦为性奴| 国产日本欧美亚洲精品视| 国产美女一区在线观看| 亚洲欧美在线视频第一页| 欧美va不卡视频在线观看| 亚洲超碰97人人做人人爱| 青青草原网站在线观看| 国产乱弄免费视频观看| av中文字幕网址在线| 99婷婷在线观看视频| 天天射夜夜操狠狠干| 传媒在线播放国产精品一区| 欧美精品激情在线最新观看视频| 中文字幕乱码人妻电影| 熟女人妻在线观看视频| 亚洲伊人久久精品影院一美女洗澡 | 中文字幕一区二区三区人妻大片| 人妻少妇性色欲欧美日韩 | 天堂av狠狠操蜜桃| 在线视频精品你懂的| 国产亚洲精品品视频在线| 4个黑人操素人视频网站精品91| 亚洲免费va在线播放| 天天操天天插天天色| 亚洲中文字幕人妻一区| 青青青视频自偷自拍38碰| 操的小逼流水的文章| 国产激情av网站在线观看| 国产实拍勾搭女技师av在线| 国产日韩欧美美利坚蜜臀懂色| av天堂资源最新版在线看| 久久久精品999精品日本| 亚洲综合在线观看免费| 91国偷自产一区二区三区精品| 日本成人不卡一区二区| 欧美一区二区三区激情啪啪啪| 中国视频一区二区三区| 国产性生活中老年人视频网站| 国产女人被做到高潮免费视频 | 9色在线视频免费观看| 狠狠躁夜夜躁人人爽天天久天啪| 天天摸天天干天天操科普| 亚洲高清自偷揄拍自拍| 国产中文字幕四区在线观看| 2020久久躁狠狠躁夜夜躁| 中文字幕人妻av在线观看| 久久久久久9999久久久久| 91久久人澡人人添人人爽乱| 天天干天天操天天爽天天摸| 阴茎插到阴道里面的视频| 日韩精品电影亚洲一区| 免费费一级特黄真人片| 夜色撩人久久7777| 青青青青青青青青青青草青青| av中文字幕电影在线看| 91试看福利一分钟| 日本www中文字幕| 精品区一区二区三区四区人妻 | 美女日逼视频免费观看| 成人伊人精品色xxxx视频| 一个色综合男人天堂| 班长撕开乳罩揉我胸好爽| 免费费一级特黄真人片| 国产又粗又硬又大视频| 中文字幕—97超碰网| 青青青青青免费视频| 午夜美女少妇福利视频| 中文字幕综合一区二区| 人妻丰满熟妇综合网| 国产视频在线视频播放| 亚洲国产精品免费在线观看| 免费人成黄页网站在线观看国产| 欧美一级视频一区二区| 亚洲精品久久视频婷婷| 熟女人妻三十路四十路人妻斩| 性色av一区二区三区久久久| 91麻豆精品久久久久| 日韩美女精品视频在线观看网站| 亚洲成av人无码不卡影片一| 经典亚洲伊人第一页| 这里有精品成人国产99| 99re久久这里都是精品视频| 欧美熟妇一区二区三区仙踪林| 91亚洲手机在线视频播放| 午夜精品一区二区三区更新| 天美传媒mv视频在线观看| 亚洲欧美一卡二卡三卡| 欧美一级色视频美日韩| 一级a看免费观看网站| 亚洲熟妇久久无码精品| 天堂av中文在线最新版| 男人天堂最新地址av| 中文字幕亚洲中文字幕| 一二三中文乱码亚洲乱码one| 青青青青青免费视频| 夫妻在线观看视频91| 高清成人av一区三区 | 97国产福利小视频合集| 天天干天天插天天谢| 美女吃鸡巴操逼高潮视频| 91高清成人在线视频| 欧美麻豆av在线播放| 日韩美在线观看视频黄| 国产成人综合一区2区| av网址在线播放大全| 乱亲女秽乱长久久久| 99人妻视频免费在线| 精品国产乱码一区二区三区乱| 亚洲乱码中文字幕在线| 自拍偷拍一区二区三区图片| heyzo蜜桃熟女人妻| 成人激情文学网人妻| 天天操夜夜骑日日摸| 国产熟妇一区二区三区av | 伊人综合免费在线视频| 欧美视频不卡一区四区| 精品人妻一二三区久久| 亚洲1区2区3区精华液| 日本av在线一区二区三区| 大学生A级毛片免费视频| 国产极品精品免费视频| 日韩欧美国产一区ab| 在线观看免费视频色97| 国产大学生援交正在播放| 午夜激情久久不卡一区二区| 韩国一级特黄大片做受| 国产黄色大片在线免费播放| www日韩a级s片av| 99精品国产自在现线观看| 亚洲天堂有码中文字幕视频| 99精品久久久久久久91蜜桃| 黄色成年网站午夜在线观看 | 美洲精品一二三产区区别| 日韩av大胆在线观看| 成人福利视频免费在线| 大陆av手机在线观看| 人妻激情图片视频小说| 亚洲麻豆一区二区三区| 啊啊好慢点插舔我逼啊啊啊视频| 超碰中文字幕免费观看| 国产精品伦理片一区二区| 欧美成人综合色在线噜噜| 99精品视频之69精品视频 | 日韩欧美在线观看不卡一区二区 | 女同互舔一区二区三区| 91免费黄片可看视频| 1024久久国产精品| 国产在线自在拍91国语自产精品| 夜夜骑夜夜操夜夜奸| 婷婷久久久综合中文字幕| 三上悠亚和黑人665番号| 欧美一区二区中文字幕电影| 66久久久久久久久久久| 天天草天天色天天干| 男人和女人激情视频| 老鸭窝在线观看一区| 夫妻在线观看视频91| 久久久麻豆精亚洲av麻花| 午夜精品久久久久久99热| 少妇露脸深喉口爆吞精| 人妻熟女在线一区二区| 97超碰人人搞人人| 2020国产在线不卡视频| 成熟丰满熟妇高潮xx×xx| 欧美黄色录像免费看的| 2o22av在线视频| 农村胖女人操逼视频| 大香蕉玖玖一区2区| 天天色天天舔天天射天天爽| av高潮迭起在线观看| avjpm亚洲伊人久久| 丁香花免费在线观看中文字幕| 欧美精品资源在线观看| 欧美日韩情色在线观看| 天天日天天干天天爱| 国产精品黄片免费在线观看| 国产精品人妻一区二区三区网站| 欧美特色aaa大片| 美女大bxxxx内射| 五十路丰满人妻熟妇| 成人av中文字幕一区| 久久免看30视频口爆视频| 喷水视频在线观看这里只有精品| 曰本无码人妻丰满熟妇啪啪| 青青青青青青青青青国产精品视频| 亚洲另类图片蜜臀av| 亚洲欧美一区二区三区电影| 中文 成人 在线 视频| 亚洲变态另类色图天堂网| 日本高清成人一区二区三区| 一区二区三区的久久的蜜桃的视频| 五十路熟女人妻一区二| 日韩av中文在线免费观看| 精品乱子伦一区二区三区免费播| av一本二本在线观看| 3D动漫精品啪啪一区二区下载| av在线shipin| 免费观看成年人视频在线观看| 91精品视频在线观看免费| 在线免费观看视频一二区| 丝袜肉丝一区二区三区四区在线 | 亚洲人妻av毛片在线| 亚洲中文字幕国产日韩| 视频久久久久久久人妻| free性日本少妇| 偷拍自拍亚洲美腿丝袜| 青青草亚洲国产精品视频| 欧美在线精品一区二区三区视频 | 亚洲va欧美va人人爽3p| 亚洲 中文 自拍 无码| 久青青草视频手机在线免费观看| 午夜激情久久不卡一区二区| 最后99天全集在线观看| 欧洲日韩亚洲一区二区三区| 搞黄色在线免费观看| 青娱乐在线免费视频盛宴 | 亚洲va天堂va国产va久| 操日韩美女视频在线免费看| 五十路熟女人妻一区二区9933| 欧美交性又色又爽又黄麻豆| av老司机亚洲一区二区| 一区二区三区久久中文字幕| av在线免费观看亚洲天堂| 在线观看视频 你懂的| 老司机你懂得福利视频| 日本一区美女福利视频| 天天操天天操天天碰| 久草福利电影在线观看| 国产一区二区火爆视频| 欧洲亚洲欧美日韩综合| 天天日天天干天天插舔舔| 粗大的内捧猛烈进出爽大牛汉子| 天天做天天干天天操天天射| 99久久超碰人妻国产| 亚洲Av无码国产综合色区| 亚洲激情av一区二区| 国产精品一区二区三区蜜臀av | 白白操白白色在线免费视频| 亚洲自拍偷拍综合色| 一区国内二区日韩三区欧美| 在线观看免费视频网| 福利视频网久久91| 日视频免费在线观看| 国产高清女主播在线| 亚洲一区二区激情在线| 97国产福利小视频合集| 国产成人精品久久二区91| 少妇人妻真实精品视频| 成人资源在线观看免费官网| 欧美黑人与人妻精品| 青青青青草手机在线视频免费看| 天天干天天插天天谢| 2020韩国午夜女主播在线| 国产91精品拍在线观看| 日本精品美女在线观看| 国产精品久久久久久美女校花| 被大鸡吧操的好舒服视频免费| 亚洲一区二区三区偷拍女厕91| 国产福利小视频大全| 婷婷五月亚洲综合在线| 91麻豆精品秘密入口在线观看| 黄色大片男人操女人逼| 午夜精品久久久久久99热| 欧美怡红院视频在线观看| 亚洲av男人天堂久久| 国产妇女自拍区在线观看 | 好男人视频在线免费观看网站| 在线视频这里只有精品自拍| 日本脱亚入欧是指什么| 在线观看911精品国产 | 国产亚洲视频在线观看| 青青青青操在线观看免费| 2021国产一区二区| 亚洲精品国产久久久久久| 久久这里只有精彩视频免费| 综合激情网激情五月五月婷婷| 污污小视频91在线观看| 国产在线自在拍91国语自产精品| 国产极品精品免费视频| 成人综合亚洲欧美一区| wwwxxx一级黄色片| 日本精品美女在线观看| 亚洲国产香蕉视频在线播放| 97精品综合久久在线| 乱亲女秽乱长久久久| 亚洲精品 日韩电影| 快插进小逼里大鸡吧视频| 91在线免费观看成人| 欧美特级特黄a大片免费| 成人30分钟免费视频| 青青尤物在线观看视频网站| 日本少妇精品免费视频| 久久一区二区三区人妻欧美| 亚洲国产中文字幕啊啊啊不行了| 日本啪啪啪啪啪啪啪| 99精品一区二区三区的区| 老师啊太大了啊啊啊尻视频| 午夜91一区二区三区| 99的爱精品免费视频| 天天操天天干天天日狠狠插 | 亚洲女人的天堂av| 噜噜色噜噜噜久色超碰| 亚洲一区二区三区精品视频在线| 国产午夜亚洲精品麻豆| 熟妇一区二区三区高清版| 超碰在线中文字幕一区二区| 日韩美av高清在线| 黄色片黄色片wyaa| 老鸭窝日韩精品视频观看| weyvv5国产成人精品的视频| 99热这里只有精品中文| 2020国产在线不卡视频| 狠狠鲁狠狠操天天晚上干干| 国产亚洲视频在线二区| 亚洲av无码成人精品区辽| 中文字幕免费福利视频6| gav成人免费播放| 久久久久久性虐视频| 大香蕉伊人中文字幕| 在线可以看的视频你懂的 | 久久久久91精品推荐99| 中文字幕在线视频一区二区三区| 在线视频精品你懂的| 又粗又硬又猛又爽又黄的| 日本av高清免费网站| 韩国男女黄色在线观看| 91色网站免费在线观看| 无码中文字幕波多野不卡| 熟女在线视频一区二区三区| 国产激情av网站在线观看| 欧美男人大鸡吧插女人视频| 丝袜国产专区在线观看| 99热色原网这里只有精品| 一个色综合男人天堂| 国产亚洲天堂天天一区| 国产一区二区久久久裸臀| 亚洲av男人的天堂你懂的| 亚洲精品欧美日韩在线播放| 亚洲一区二区三区偷拍女厕91| 在线免费91激情四射| 中文字幕日韩精品就在这里| 欧美一区二区三区啪啪同性| 国产激情av网站在线观看| 五十路息与子猛烈交尾视频 | 啪啪啪啪啪啪啪免费视频| 很黄很污很色的午夜网站在线观看| 天天色天天操天天舔| 又粗又长 明星操逼小视频| 一级黄色片夫妻性生活| 欧美性受xx黑人性猛交| 国产精品一二三不卡带免费视频 | av高潮迭起在线观看| 亚洲国产成人无码麻豆艾秋| 亚洲精品亚洲人成在线导航 | 亚洲精品欧美日韩在线播放| 久久免费看少妇高潮完整版| 亚洲欧美国产综合777| 天天日天天做天天日天天做| 国产精品久久久久国产三级试频 | 中文字幕乱码av资源| 久精品人妻一区二区三区 | 日本人妻少妇18—xx| 东京干手机福利视频| 日韩成人性色生活片| 香港三日本三韩国三欧美三级| 色哟哟在线网站入口| 在线免费观看日本伦理| 超碰在线中文字幕一区二区| 国产亚洲精品视频合集| 精品国产乱码一区二区三区乱| 同居了嫂子在线播高清中文| 青青青青青手机视频| 深夜男人福利在线观看| 亚洲va国产va欧美va在线| 最新欧美一二三视频| 亚洲蜜臀av一区二区三区九色| 久久永久免费精品人妻专区| 国产久久久精品毛片| 午夜精品一区二区三区城中村| 在线国产精品一区二区三区| 国产va精品免费观看 | 九一传媒制片厂视频在线免费观看| 欧美黑人性暴力猛交喷水| 中文字幕中文字幕 亚洲国产| 久久久久久久精品老熟妇| 欧美3p在线观看一区二区三区| 亚洲欧洲一区二区在线观看| 精品亚洲在线免费观看| 国产女人叫床高潮大片视频| 免费69视频在线看| xxx日本hd高清| 老司机福利精品免费视频一区二区| 中文字幕乱码人妻电影| 成人av亚洲一区二区| 亚洲福利天堂久久久久久| 97小视频人妻一区二区| 大香蕉大香蕉在线看| 国产日本精品久久久久久久| 中文字幕av男人天堂| 青娱乐极品视频青青草| 蜜桃视频入口久久久| 一区二区三区av高清免费| 视频 一区二区在线观看| 在线免费91激情四射 | 97精品综合久久在线| 馒头大胆亚洲一区二区| 亚洲成a人片777777| 国产免费高清视频视频| 久久久久久99国产精品| 老司机在线精品福利视频| 大陆精品一区二区三区久久| 日曰摸日日碰夜夜爽歪歪| 亚洲日本一区二区三区| 国产av一区2区3区| 色婷婷精品大在线观看| 久久h视频在线观看| 亚洲国产精品美女在线观看| 丰满的继坶3中文在线观看| 中文字幕奴隷色的舞台50| 夜夜骑夜夜操夜夜奸| 亚洲精品欧美日韩在线播放| 91国语爽死我了不卡| 国产精品久久久久网| 成人免费毛片aaaa| 国产精品污污污久久| 中文字幕第一页国产在线| 少妇被强干到高潮视频在线观看| 特大黑人巨大xxxx| 国产亚洲天堂天天一区| 青青草国内在线视频精选| 国产黄网站在线观看播放| 国产91精品拍在线观看| 91大神福利视频网| 毛茸茸的大外阴中国视频| 一色桃子人妻一区二区三区| 中国黄片视频一区91| av一本二本在线观看| 超级碰碰在线视频免费观看| 摧残蹂躏av一二三区| 免费在线黄色观看网站| 黄色男人的天堂视频| 亚洲人妻国产精品综合| 93视频一区二区三区| 天天干天天操天天爽天天摸| 狠狠躁狠狠爱网站视频| 夜鲁夜鲁狠鲁天天在线| 久草视频 久草视频2| 亚洲综合另类欧美久久| 国产精品一区二区av国| 极品粉嫩小泬白浆20p主播| 日本中文字幕一二区视频| 日本a级视频老女人| 精品美女在线观看视频在线观看 | 国产高潮无码喷水AV片在线观看| 亚洲成人国产综合一区| 国产精品人久久久久久| 超pen在线观看视频公开97| 91大神福利视频网| 成年人免费看在线视频| 亚洲 色图 偷拍 欧美| 欧美3p在线观看一区二区三区| 欧美日韩中文字幕欧美| 国产大学生援交正在播放| 日韩精品中文字幕在线| 无码国产精品一区二区高潮久久4 日韩欧美一级精品在线观看 | 91国产在线视频免费观看| 天天日天天做天天日天天做| 亚洲欧美激情人妻偷拍| 亚洲精品福利网站图片| 日本黄色特一级视频| 国产亚洲欧美45p| 亚洲欧洲av天堂综合| 国产精品黄色的av| 亚洲综合另类欧美久久| 91精品国产观看免费| 国内自拍第一页在线观看| 免费男阳茎伸入女阳道视频| 青青青艹视频在线观看| 9l人妻人人爽人人爽| 91精品国产观看免费| 亚洲另类综合一区小说| 1769国产精品视频免费观看| 制丝袜业一区二区三区| 护士特殊服务久久久久久久| 综合精品久久久久97| 国产精品自偷自拍啪啪啪| 999久久久久999| 色婷婷精品大在线观看| 国产妇女自拍区在线观看| 国产成人精品久久二区91| mm131美女午夜爽爽爽| 中英文字幕av一区| 少妇人妻真实精品视频| 99热久久极品热亚洲| 97资源人妻免费在线视频| 操日韩美女视频在线免费看| 日本女人一级免费片| aⅴ精产国品一二三产品| 久久热这里这里只有精品| 2020av天堂网在线观看| 特一级特级黄色网片| 动漫美女的小穴视频| 91久久精品色伊人6882| 热思思国产99re| 欧美日本国产自视大全| 阴茎插到阴道里面的视频| 中文字幕亚洲久久久| 这里只有精品双飞在线播放| 免费在线看的黄网站| 888欧美视频在线| 男女啪啪视频免费在线观看 | 国产一区二区久久久裸臀| 亚洲特黄aaaa片| 天堂v男人视频在线观看| 伊人网中文字幕在线视频| 中文字幕免费在线免费| 婷婷五月亚洲综合在线| 精品一区二区三四区| 亚洲一区二区三区精品视频在线| 精品人妻伦一二三区久| 大尺度激情四射网站| www日韩毛片av| 在线亚洲天堂色播av电影| 一二三中文乱码亚洲乱码one| 在线观看亚洲人成免费网址| 91成人精品亚洲国产| 国产精品入口麻豆啊啊啊| 国产一区av澳门在线观看| 亚洲高清国产自产av| 美女日逼视频免费观看| 天天操夜夜操天天操天天操 | 国产揄拍高清国内精品对白| 欧美成人精品欧美一级黄色| 青青色国产视频在线| 3344免费偷拍视频| 91色网站免费在线观看| 欧美天堂av无线av欧美| 精品久久久久久高潮| 在线新三级黄伊人网| 男女啪啪视频免费在线观看 | 国内精品在线播放第一页| 3D动漫精品啪啪一区二区下载| 丝袜肉丝一区二区三区四区在线| 日韩精品二区一区久久| 亚洲中文字字幕乱码| 国产免费高清视频视频| 国产熟妇一区二区三区av | 99热99re在线播放| 视频二区在线视频观看| 一区二区三区视频,福利一区二区| 天天操天天射天天操天天天| 免费看国产av网站| 亚洲午夜伦理视频在线| 天天爽夜夜爽人人爽QC| 国产大学生援交正在播放| 在线不卡日韩视频播放| 亚洲推理片免费看网站| 在线播放国产黄色av| 91传媒一区二区三区| 97欧洲一区二区精品免费| 91福利在线视频免费观看| 韩国三级aaaaa高清视频 | 日本熟妇喷水xxx| 国产精品一区二区三区蜜臀av| 男女第一次视频在线观看| 区一区二区三国产中文字幕| 亚洲国产40页第21页| 亚洲免费视频欧洲免费视频| 中国把吊插入阴蒂的视频| 久久久精品精品视频视频| 日本午夜爽爽爽爽爽视频在线观看 | 又色又爽又黄又刺激av网站| 天天日天天干天天搡| 亚洲公开视频在线观看| 在线国产日韩欧美视频| 青青草在观免费国产精品| AV无码一区二区三区不卡| 精品首页在线观看视频| 日本真人性生活视频免费看| 99久久久无码国产精品性出奶水 | 沙月文乃人妻侵犯中文字幕在线| 国产激情av网站在线观看| 亚洲美女高潮喷浆视频| 亚洲成人熟妇一区二区三区| 黄页网视频在线免费观看| 欧美精品免费aaaaaa| 国产第一美女一区二区三区四区| 少妇与子乱在线观看| 婷婷久久久久深爱网| 亚洲精品ww久久久久久| 日韩a级精品一区二区| 青青青国产免费视频| 亚洲人妻视频在线网| 91 亚洲视频在线观看| 精品亚洲中文字幕av| tube69日本少妇| chinese国产盗摄一区二区| 97少妇精品在线观看| 青青草视频手机免费在线观看| 宅男噜噜噜666国产| 黄色片一级美女黄色片| 日韩精品啪啪视频一道免费| 欧美久久久久久三级网| 美女操逼免费短视频下载链接| 日噜噜噜夜夜噜噜噜天天噜噜噜| 国产精品亚洲在线观看| 老司机福利精品免费视频一区二区 | 一区二区视频在线观看视频在线| 亚洲午夜在线视频福利| 天天干天天日天天干天天操| 日本精品美女在线观看| 第一福利视频在线观看| 四川乱子伦视频国产vip| 日韩精品激情在线观看| 女同久久精品秋霞网| 午夜福利人人妻人人澡人人爽| 视频一区 二区 三区 综合| 偷拍美女一区二区三区| 免费黄色成人午夜在线网站| 啪啪啪18禁一区二区三区 | 日本在线一区二区不卡视频| 清纯美女在线观看国产| 精品suv一区二区69| 91精品国产91久久自产久强| 馒头大胆亚洲一区二区| 动漫美女的小穴视频| 97超碰国语国产97超碰| 十八禁在线观看地址免费| 9久在线视频只有精品| 日韩成人免费电影二区| 国产麻豆精品人妻av| 综合国产成人在线观看| 端庄人妻堕落挣扎沉沦| 亚洲欧美一区二区三区电影| 中文字幕之无码色多多| 欧美一区二区三区四区性视频| mm131美女午夜爽爽爽| 欧美3p在线观看一区二区三区| 免费成人av中文字幕| 特黄老太婆aa毛毛片| 久久久久久久久久一区二区三区| 婷婷色中文亚洲网68| 精品人妻伦一二三区久 | av中文字幕在线观看第三页 | 任我爽精品视频在线播放| 97国产在线av精品| 天天日天天透天天操| okirakuhuhu在线观看| 日韩美女精品视频在线观看网站 | 大屁股肉感人妻中文字幕在线| 93人妻人人揉人人澡人人| 亚洲va国产va欧美精品88| 日本高清在线不卡一区二区| 成人综合亚洲欧美一区| 亚洲国产精品黑丝美女| 操操网操操伊剧情片中文字幕网| 精品suv一区二区69| 欧美熟妇一区二区三区仙踪林| 亚洲av第国产精品| 欧美日本aⅴ免费视频| 天天日天天干天天舔天天射| 免费在线播放a级片| 日本一区精品视频在线观看| 亚洲伊人色一综合网| 亚洲码av无色中文| 一区二区三区av高清免费| 91大屁股国产一区二区| 国产麻豆国语对白露脸剧情 | 天天插天天色天天日| 欧美一区二区三区激情啪啪啪| 亚洲av人人澡人人爽人人爱| 天天日天天操天天摸天天舔| 一区二区三区四区视频| 中国黄色av一级片| 天堂中文字幕翔田av| 一二三中文乱码亚洲乱码one| 亚洲免费国产在线日韩| 久久国产精品精品美女| 在线视频国产欧美日韩| 懂色av之国产精品| 国产aⅴ一线在线观看| 午夜精品久久久久久99热| 国际av大片在线免费观看| 美女视频福利免费看| 青青草原网站在线观看| 久草视频 久草视频2| 99热99这里精品6国产| 精品91自产拍在线观看一区| 黄色的网站在线免费看| 日韩欧美在线观看不卡一区二区| 成年午夜免费无码区| 97资源人妻免费在线视频| 福利视频广场一区二区| 青青热久免费精品视频在线观看 | 免费国产性生活视频| 欧美xxx成人在线| 动漫av网站18禁| 精品91自产拍在线观看一区| 伊人情人综合成人久久网小说| 2025年人妻中文字幕乱码在线| 青青草人人妻人人妻| 日韩欧美在线观看不卡一区二区| 亚洲色偷偷综合亚洲AV伊人| 亚洲人妻视频在线网| 午夜在线一区二区免费| 夫妻在线观看视频91| 欧美黄片精彩在线免费观看| 888欧美视频在线| 1区2区3区不卡视频| 午夜dv内射一区区| 这里只有精品双飞在线播放| 日韩成人性色生活片| av手机在线观播放网站| 国产精品久久久久久久久福交 | 日韩精品激情在线观看| 免费手机黄页网址大全| 中文字幕高清资源站| 青青青国产免费视频| 97成人免费在线观看网站| 久久精品美女免费视频| 在线不卡日韩视频播放| 日韩精品激情在线观看| 日本韩国免费一区二区三区视频| 精品欧美一区二区vr在线观看| 青青青青青手机视频| 色天天天天射天天舔| 欧美偷拍亚洲一区二区| 黑人解禁人妻叶爱071| 少妇露脸深喉口爆吞精| 国产污污污污网站在线| av久久精品北条麻妃av观看| 人妻爱爱 中文字幕| 中文字幕av熟女人妻| 日韩亚洲高清在线观看| 91福利视频免费在线观看| 一级黄色av在线观看| 日韩加勒比东京热二区| 中文亚洲欧美日韩无线码| 欧美一区二区中文字幕电影| 色吉吉影音天天干天天操| 欧洲欧美日韩国产在线| 亚洲中文字字幕乱码| 久久www免费人成一看片| 99热碰碰热精品a中文| 午夜国产福利在线观看| 欧美成人一二三在线网| 中文字幕中文字幕 亚洲国产| 香蕉91一区二区三区| 99精品亚洲av无码国产另类| 亚洲午夜福利中文乱码字幕| 国产在线拍揄自揄视频网站| 五色婷婷综合狠狠爱| 2020中文字幕在线播放| 国产日本精品久久久久久久| 国产高清在线在线视频| 日本后入视频在线观看| 天天日天天玩天天摸| 日韩av有码一区二区三区4| 午夜在线一区二区免费| 亚洲午夜精品小视频| 深田咏美亚洲一区二区| 精品人妻每日一部精品| 欧美精品中文字幕久久二区| 亚洲精品久久视频婷婷| 成年女人免费播放视频| 国产精品黄片免费在线观看| 亚洲福利精品福利精品福利| 亚洲国产香蕉视频在线播放| 欧美亚洲偷拍自拍色图| 婷婷色中文亚洲网68| 欧美男同性恋69视频| 搡老妇人老女人老熟女| 亚洲第一黄色在线观看| 欧美一区二区三区乱码在线播放| 欧美麻豆av在线播放| 日本韩国在线观看一区二区| 一区二区免费高清黄色视频| 亚洲av人人澡人人爽人人爱| 桃色视频在线观看一区二区| 91精品国产91久久自产久强| 一区二区三区毛片国产一区| 男人和女人激情视频| chinese国产盗摄一区二区| 精品区一区二区三区四区人妻| 成人福利视频免费在线| 绝顶痉挛大潮喷高潮无码 | 国产成人一区二区三区电影网站 | 亚洲国产精品美女在线观看| 专门看国产熟妇的网站| 真实国模和老外性视频| 午夜精品久久久久麻豆影视| 亚洲精品在线资源站| 亚洲综合自拍视频一区| 国产又色又刺激在线视频 | 亚洲一区av中文字幕在线观看| 久久99久久99精品影院| 啪啪啪啪啪啪啪免费视频| 青青青青青免费视频| 一区二区三区视频,福利一区二区| 91天堂天天日天天操| 在线视频这里只有精品自拍| 一区二区在线观看少妇| 欧美成人黄片一区二区三区| 天天干天天搞天天摸| 真实国产乱子伦一区二区| 中文字幕免费在线免费| 国产一区成人在线观看视频| 国产片免费观看在线观看| 精品一区二区三区欧美| 中文字幕第1页av一天堂网| av中文字幕在线观看第三页| 五十路人妻熟女av一区二区| 中文字幕AV在线免费看 | 中文字幕中文字幕 亚洲国产| 欧美精品欧美极品欧美视频| 亚洲成人熟妇一区二区三区| 国产精品久久综合久久| 91试看福利一分钟| 日韩少妇人妻精品无码专区| 国产a级毛久久久久精品| 亚洲中文字幕综合小综合| 亚洲激情av一区二区| 黑人变态深video特大巨大| 国产97在线视频观看| 18禁精品网站久久| 激情色图一区二区三区| 欧美一区二区三区四区性视频| 五十路老熟女码av| 久久久久91精品推荐99| 91高清成人在线视频| 爆乳骚货内射骚货内射在线| 国产欧美日韩在线观看不卡| 99久久成人日韩欧美精品| 日本av高清免费网站| 中英文字幕av一区| 啪啪啪啪啪啪啪免费视频| 婷婷六月天中文字幕| 自拍偷拍亚洲另类色图| 黄网十四区丁香社区激情五月天| 97人妻人人澡爽人人精品| 精品黑人一区二区三区久久国产 | 免费一级黄色av网站| 亚洲一区自拍高清免费视频| 国产麻豆乱子伦午夜视频观看| lutube在线成人免费看| 一区二区在线视频中文字幕| 国产之丝袜脚在线一区二区三区| 欧美激情电影免费在线| 制服丝袜在线人妻中文字幕| 首之国产AV医生和护士小芳| 沈阳熟妇28厘米大战黑人| 欧美在线精品一区二区三区视频| 38av一区二区三区| 免费69视频在线看| 国产一区二区火爆视频| 中文字幕人妻被公上司喝醉在线| 亚洲国产在线精品国偷产拍| 精品少妇一二三视频在线| 97人妻无码AV碰碰视频| 精品久久久久久久久久久99| 亚洲av日韩av第一区二区三区| 91精品高清一区二区三区| 97年大学生大白天操逼| 欧美一级片免费在线成人观看| 91 亚洲视频在线观看| 啪啪啪操人视频在线播放| 2018在线福利视频| 在线免费视频 自拍| av资源中文字幕在线观看| 老熟妇凹凸淫老妇女av在线观看| 青青社区2国产视频| 伊人网中文字幕在线视频| 老鸭窝日韩精品视频观看| 521精品视频在线观看| 最新97国产在线视频| 日韩一区二区电国产精品| 亚洲福利天堂久久久久久| 爆乳骚货内射骚货内射在线| 久久永久免费精品人妻专区| 亚洲一区二区激情在线| 精品视频国产在线观看| 99视频精品全部15| 熟女在线视频一区二区三区| 9色在线视频免费观看| 91极品新人『兔兔』精品新作| 91在线视频在线精品3| 中文字幕 码 在线视频| 99热99re在线播放| 日韩欧美国产精品91| 77久久久久国产精产品| 蜜桃视频入口久久久| 国产真实灌醉下药美女av福利| 免费观看丰满少妇做受| 第一福利视频在线观看| 国产chinesehd精品麻豆| 五十路息与子猛烈交尾视频| 欧亚乱色一区二区三区| 欧美精品久久久久久影院| 人妻少妇中文有码精品| 国产精品中文av在线播放 | 国产精品人妻熟女毛片av久| 天天操天天射天天操天天天| 在线免费观看靠比视频的网站| 久久久久久97三级| 国产日本欧美亚洲精品视| 91国产资源在线视频| 免费手机黄页网址大全| 天堂va蜜桃一区入口| 天天躁日日躁狠狠躁av麻豆| 日韩美在线观看视频黄| 色噜噜噜噜18禁止观看| 在线免费观看视频一二区| 日本少妇人妻xxxxx18| 国产av欧美精品高潮网站| 精品av国产一区二区三区四区| 精品91自产拍在线观看一区| 97超碰免费在线视频| 日本在线一区二区不卡视频| 香蕉aⅴ一区二区三区| 在线播放 日韩 av| 久久永久免费精品人妻专区| 天天射夜夜操狠狠干| av日韩在线观看大全| 日韩欧美制服诱惑一区在线| 成人激情文学网人妻| 狠狠嗨日韩综合久久| 亚洲av日韩精品久久久久久hd| 亚洲伊人色一综合网| 色婷婷精品大在线观看| 久久香蕉国产免费天天| 自拍偷拍亚洲另类色图| 1769国产精品视频免费观看| 天天日天天干天天舔天天射| 福利国产视频在线观看| 亚洲av极品精品在线观看| 在线 中文字幕 一区| 老熟妇xxxhd老熟女| 中文字幕熟女人妻久久久| 亚洲 欧美 精品 激情 偷拍| 天堂v男人视频在线观看| 人妻3p真实偷拍一二区| 婷婷五月亚洲综合在线| 中国熟女@视频91| 97小视频人妻一区二区| 午夜久久久久久久99| 五十路老熟女码av| 都市家庭人妻激情自拍视频| 大鸡八强奸视频在线观看| 国产极品精品免费视频| 一区二区麻豆传媒黄片| 国产精品污污污久久| 亚洲码av无色中文| 欧美香蕉人妻精品一区二区| 欧美第一页在线免费观看视频| gogo国模私拍视频| 亚洲国产香蕉视频在线播放| 极品丝袜一区二区三区| 成年人啪啪视频在线观看| aaa久久久久久久久| 美女av色播在线播放| 亚洲熟女综合色一区二区三区四区| 亚洲人妻国产精品综合| 亚洲一区二区三区精品乱码| 日韩美女搞黄视频免费| 最近中文字幕国产在线| 欧亚日韩一区二区三区观看视频| 欧美性感尤物人妻在线免费看| 亚洲激情偷拍一区二区| 在线国产中文字幕视频| 亚洲国产精品黑丝美女| 少妇露脸深喉口爆吞精| 国产免费av一区二区凹凸四季| 国产夫妻视频在线观看免费 | 亚洲午夜电影在线观看| av网址国产在线观看| 国产亚洲欧美视频网站| 中文字幕高清免费在线人妻| 狍和女人的王色毛片| 中文字幕第三十八页久久| 国产成人无码精品久久久电影| 亚洲少妇高潮免费观看| 中文字幕日韩91人妻在线| 亚洲精品国产在线电影| 成熟熟女国产精品一区| 2020av天堂网在线观看| 日韩av有码中文字幕| 首之国产AV医生和护士小芳| 夜夜操,天天操,狠狠操| 激情啪啪啪啪一区二区三区| gay gay男男瑟瑟在线网站| 亚洲高清视频在线不卡| 深田咏美亚洲一区二区| 视频在线免费观看你懂得| 五十路人妻熟女av一区二区| 亚洲高清国产拍青青草原| 国产精品国产三级国产精东| 青青青艹视频在线观看| 日日操综合成人av| 不卡一不卡二不卡三| 亚洲成高清a人片在线观看| 日日摸夜夜添夜夜添毛片性色av| 天天色天天操天天透| 国产美女午夜福利久久| 人妻凌辱欧美丰满熟妇| 绝顶痉挛大潮喷高潮无码| 337p日本大胆欧美人| 青青在线视频性感少妇和隔壁黑丝| 黄色资源视频网站日韩| 夜色17s精品人妻熟女| 欧美日本aⅴ免费视频| 啪啪啪18禁一区二区三区| 99国内小视频在现欢看| 国产成人精品午夜福利训2021| 天干天天天色天天日天天射| 天天做天天干天天操天天射| 99久久激情婷婷综合五月天| 国产女孩喷水在线观看| 日本精品美女在线观看| 男女啪啪啪啪啪的网站| 新97超碰在线观看| 日日日日日日日日夜夜夜夜夜夜| 亚洲天堂第一页中文字幕| 国产av福利网址大全| 插小穴高清无码中文字幕| 88成人免费av网站| 大黑人性xxxxbbbb| 国产chinesehd精品麻豆| 日本啪啪啪啪啪啪啪| 成人高潮aa毛片免费| 亚洲一区二区三区五区| 亚洲中文字幕国产日韩| 黄片色呦呦视频免费看| 丁香花免费在线观看中文字幕| 国产高潮无码喷水AV片在线观看| 大香蕉玖玖一区2区| 国产精品欧美日韩区二区| 91精品高清一区二区三区| 人人人妻人人澡人人| 大陆精品一区二区三区久久| 视频一区二区综合精品| 日本欧美视频在线观看三区| 免费观看理论片完整版| 精品一区二区三区午夜| 93人妻人人揉人人澡人人| wwwxxx一级黄色片| 欧美综合婷婷欧美综合| 亚洲伊人久久精品影院一美女洗澡| 天天射夜夜操狠狠干| 女生被男生插的视频网站| 97少妇精品在线观看| 欧美成人综合视频一区二区 | 欧美80老妇人性视频| 国产一区二区欧美三区| 亚洲丝袜老师诱惑在线观看| 中文字幕在线第一页成人| 国产av福利网址大全| 日韩精品中文字幕福利| 一区二区熟女人妻视频| 大香蕉福利在线观看| 18禁网站一区二区三区四区 | av中文字幕电影在线看| 99热碰碰热精品a中文| 51国产成人精品视频| 在线观看国产网站资源| 国产日韩欧美美利坚蜜臀懂色| 93精品视频在线观看| 亚洲一级av大片免费观看| 精品国产在线手机在线| 日辽宁老肥女在线观看视频| 91人妻精品一区二区在线看| 久久国产精品精品美女| 免费69视频在线看| 亚洲 自拍 色综合图| 人妻丝袜精品中文字幕| 高潮视频在线快速观看国家快速| 亚洲福利精品福利精品福利| 欧美日韩在线精品一区二区三| 人妻少妇精品久久久久久| 久久久麻豆精亚洲av麻花| 亚洲高清国产拍青青草原| 鸡巴操逼一级黄色气| 最新激情中文字幕视频| 黄片色呦呦视频免费看| 97年大学生大白天操逼| 欧美一区二区中文字幕电影| 色花堂在线av中文字幕九九 | 亚洲成人激情av在线| 国产三级精品三级在线不卡| 亚洲成人线上免费视频观看| 亚洲偷自拍高清视频| 中文字幕av一区在线观看| 国产综合视频在线看片| 午夜场射精嗯嗯啊啊视频| 午夜国产福利在线观看| 国产黄网站在线观看播放| 午夜精品久久久久久99热| 久久香蕉国产免费天天| 国产精品自拍偷拍a| 欧洲日韩亚洲一区二区三区 | 男女之间激情网午夜在线| 晚上一个人看操B片| 欧美成人综合视频一区二区| 91国内精品久久久久精品一| 视频 一区二区在线观看| 一二三区在线观看视频| 国产精品视频欧美一区二区| 91大神福利视频网| 中文字幕中文字幕 亚洲国产| 日韩成人综艺在线播放| 男大肉棒猛烈插女免费视频| 国产卡一卡二卡三乱码手机| 在线成人日韩av电影| www日韩毛片av| 亚洲精品乱码久久久久久密桃明| 中文字幕在线永久免费播放| 亚洲av色图18p| 91精品资源免费观看| 亚洲成人午夜电影在线观看 | 亚洲av无硬久久精品蜜桃| 亚洲成人熟妇一区二区三区| 成年人啪啪视频在线观看| ka0ri在线视频| 国产成人一区二区三区电影网站| 一区二区三区毛片国产一区| 欧美熟妇一区二区三区仙踪林| 男女啪啪啪啪啪的网站| 91免费观看在线网站| 自拍偷拍日韩欧美一区二区| 阿v天堂2014 一区亚洲| 99精品国产aⅴ在线观看| 免费观看成年人视频在线观看| 91破解版永久免费| 亚洲av人人澡人人爽人人爱| 欧美日韩一级黄片免费观看| 一区二区三区毛片国产一区| 91色秘乱一区二区三区| 99精品国自产在线人| 免费一级特黄特色大片在线观看| 91精品国产麻豆国产| 中文字幕人妻熟女在线电影| 2022精品久久久久久中文字幕| 最新的中文字幕 亚洲| av乱码一区二区三区| 日本xx片在线观看| 黄色成年网站午夜在线观看| 日韩欧美一级aa大片| 六月婷婷激情一区二区三区| 欧美乱妇无乱码一区二区| 成人免费公开视频无毒| 亚洲av日韩av第一区二区三区| 亚洲一区二区三区久久受 | 亚洲一区二区三区偷拍女厕91| 久久久久久久精品成人热| 11久久久久久久久久久| 亚洲老熟妇日本老妇| 国产又色又刺激在线视频 | jiujiure精品视频在线| 亚洲乱码中文字幕在线| 成人亚洲精品国产精品| 在线制服丝袜中文字幕| 亚洲成人熟妇一区二区三区| 亚洲精品麻豆免费在线观看| 一区二区在线观看少妇| 日日夜夜狠狠干视频| 好吊视频—区二区三区| 国产白嫩美女一区二区| 国产普通话插插视频| 中文字幕一区二区自拍| 亚洲粉嫩av一区二区三区| 亚洲成人激情av在线| 国产精品久久综合久久| 欧美一区二区三区啪啪同性| 精品欧美一区二区vr在线观看| 91精品国产综合久久久蜜| 综合精品久久久久97| 日比视频老公慢点好舒服啊| 9色在线视频免费观看| 插逼视频双插洞国产操逼插洞| 亚洲高清国产一区二区三区| 97香蕉碰碰人妻国产樱花| 欧美日韩情色在线观看| 青青青艹视频在线观看| 无码中文字幕波多野不卡| 成人激情文学网人妻| 国产精品自偷自拍啪啪啪| 国产视频网站国产视频| 啪啪啪啪啪啪啪啪啪啪黄色| 亚洲精品av在线观看| 天天艹天天干天天操| 久久久久久97三级| 人妻久久久精品69系列| 在线观看亚洲人成免费网址| 自拍偷拍一区二区三区图片| 又黄又刺激的午夜小视频| 在线免费观看国产精品黄色| 91人妻精品久久久久久久网站| 国产白袜脚足J棉袜在线观看| 人妻少妇一区二区三区蜜桃| 久久久久久久精品成人热| 香港一级特黄大片在线播放| 美女在线观看日本亚洲一区| 亚洲青青操骚货在线视频| 唐人色亚洲av嫩草| av手机在线免费观看日韩av| 狠狠鲁狠狠操天天晚上干干| 日本少妇人妻xxxxx18| 狠狠鲁狠狠操天天晚上干干| 狍和女人的王色毛片| 99热碰碰热精品a中文| 18禁美女无遮挡免费| 在线免费91激情四射 | 青青草原网站在线观看| 欧美亚洲少妇福利视频| 中文亚洲欧美日韩无线码| 91国语爽死我了不卡| 中文字幕乱码人妻电影| 亚洲免费在线视频网站| 午夜精品九一唐人麻豆嫩草成人 | av资源中文字幕在线观看| 久久久人妻一区二区| 亚洲高清视频在线不卡| 天干天天天色天天日天天射| 亚洲1区2区3区精华液| 精品suv一区二区69| 93精品视频在线观看| 美女少妇亚洲精选av| 年轻的人妻被夫上司侵犯| 免费手机黄页网址大全| 特级欧美插插插插插bbbbb| 亚洲欧美色一区二区| 自拍偷拍 国产资源| 少妇系列一区二区三区视频| 亚洲欧美清纯唯美另类 | 99视频精品全部15| 综合激情网激情五月五月婷婷| 人妻另类专区欧美制服| 操人妻嗷嗷叫视频一区二区| 最新欧美一二三视频| 99热99这里精品6国产| 亚洲男人在线天堂网| 自拍偷拍日韩欧美亚洲| 同居了嫂子在线播高清中文| 狠狠地躁夜夜躁日日躁| 91免费福利网91麻豆国产精品| 中文字幕高清在线免费播放| 久久精品国产999| av天堂中文字幕最新| 年轻的人妻被夫上司侵犯| 成人网18免费视频版国产| 亚洲免费在线视频网站| 亚洲av成人网在线观看| 91片黄在线观看喷潮| 欧美特级特黄a大片免费| 亚洲视频在线观看高清| 搡老熟女一区二区在线观看| 国产又粗又硬又大视频| 亚洲熟妇无码一区二区三区| 五十路av熟女松本翔子| 国产性色生活片毛片春晓精品| 男人的天堂av日韩亚洲| 爆乳骚货内射骚货内射在线| 欧美激情电影免费在线| 护士特殊服务久久久久久久| 国产真实乱子伦a视频| 一区二区在线观看少妇| 色花堂在线av中文字幕九九 | 日韩在线视频观看有码在线 | 日本人妻欲求不满中文字幕| 国产激情av网站在线观看| 啪啪啪18禁一区二区三区| 老司机99精品视频在线观看| av高潮迭起在线观看| 久久久久久久精品成人热| 亚洲激情av一区二区| 日韩欧美在线观看不卡一区二区| 9久在线视频只有精品| 亚洲视频在线观看高清| 在线网站你懂得老司机| 午夜精彩视频免费一区| 操日韩美女视频在线免费看| 美女视频福利免费看| 沈阳熟妇28厘米大战黑人| 99国内小视频在现欢看| 精品高潮呻吟久久av| 淫秽激情视频免费观看| 99亚洲美女一区二区三区| 精品成人啪啪18免费蜜臀| 欧美成人精品欧美一级黄色| 在线观看免费视频网| 91老熟女连续高潮对白| 亚洲精品国品乱码久久久久| 99热这里只有国产精品6| 93人妻人人揉人人澡人人| 中文字幕在线乱码一区二区| 日韩av有码一区二区三区4| 国产使劲操在线播放| 免费岛国喷水视频在线观看 | 欧美交性又色又爽又黄麻豆| 在线视频国产欧美日韩| 亚洲人妻30pwc| 国产日韩精品电影7777| gav成人免费播放| 91老熟女连续高潮对白| 欧美一级片免费在线成人观看| 最新激情中文字幕视频| 欧美专区第八页一区在线播放| 欧美美女人体视频一区| 好吊操视频这里只有精品| 中文乱理伦片在线观看| 视频一区二区在线免费播放| 中文字幕人妻三级在线观看| 热久久只有这里有精品| 玩弄人妻熟妇性色av少妇| 亚洲综合乱码一区二区| 99久久久无码国产精品性出奶水| 亚洲精品高清自拍av| 亚洲va国产va欧美精品88| 97成人免费在线观看网站| 天天躁夜夜躁日日躁a麻豆| 黄色黄色黄片78在线| 青青青青草手机在线视频免费看| 成年人免费看在线视频| 国产女人露脸高潮对白视频| 91快播视频在线观看| 中文字幕一区二区亚洲一区| 啊啊啊视频试看人妻| 性色蜜臀av一区二区三区| 欧美中文字幕一区最新网址| 91精品啪在线免费| 日本熟妇色熟妇在线观看| 午夜av一区二区三区| 成年午夜免费无码区| 欧美老妇精品另类不卡片| 天天日天天干天天舔天天射| av男人天堂狠狠干| 福利午夜视频在线合集| 极品丝袜一区二区三区| 夜夜骑夜夜操夜夜奸| 久久美欧人妻少妇一区二区三区| 国产日韩一区二区在线看| 成人色综合中文字幕| 天天日天天操天天摸天天舔| 91极品大一女神正在播放| 内射久久久久综合网| av手机免费在线观看高潮| 久久久久久cao我的性感人妻| 精品国产高潮中文字幕| 亚洲福利天堂久久久久久 | 欧美亚洲偷拍自拍色图| 888亚洲欧美国产va在线播放| 亚洲人妻国产精品综合| 无码精品一区二区三区人| 人妻少妇中文有码精品| 直接能看的国产av| 在线观看国产网站资源| 久久久人妻一区二区| 男女第一次视频在线观看| 中文字幕午夜免费福利视频| 超污视频在线观看污污污| 38av一区二区三区| 大香蕉伊人中文字幕| 黄色成人在线中文字幕| 深夜男人福利在线观看| 欧美视频中文一区二区三区| 丝袜美腿视频诱惑亚洲无| 国产精品久久9999| 中文字幕无码一区二区免费| 人妻久久无码中文成人| 欧美亚洲偷拍自拍色图| 日韩成人免费电影二区| 日日夜夜狠狠干视频| 3344免费偷拍视频| 边摸边做超爽毛片18禁色戒| 欧美偷拍亚洲一区二区| 91大屁股国产一区二区| 欧美在线精品一区二区三区视频 | 亚洲精品成人网久久久久久小说| 老司机99精品视频在线观看| 亚洲高清自偷揄拍自拍| 内射久久久久综合网| 青娱乐蜜桃臀av色| 成年人免费看在线视频| 久久美欧人妻少妇一区二区三区| 2022国产综合在线干| 国产高清在线在线视频| 538精品在线观看视频| 中文字幕一区二区三区蜜月| 久久久久只精品国产三级| 91快播视频在线观看| AV天堂一区二区免费试看| 中文字幕av第1页中文字幕| 国产内射中出在线观看| 亚洲欧美清纯唯美另类| 亚洲国产精品久久久久久6| 久久这里只有精品热视频| 特大黑人巨大xxxx| 亚洲欧美激情国产综合久久久| 欧美第一页在线免费观看视频| 日韩美女综合中文字幕pp| 精品一区二区三区三区色爱| 亚洲精品久久视频婷婷| 欧美性受xx黑人性猛交| 欧美成人综合视频一区二区| 啊啊好大好爽啊啊操我啊啊视频| 青青色国产视频在线| 一本一本久久a久久精品综合不卡| 久久久精品精品视频视频| 亚洲成a人片777777| 丝袜美腿视频诱惑亚洲无| 亚洲丝袜老师诱惑在线观看| 东京热男人的av天堂| 视频在线亚洲一区二区| 亚洲精品久久综合久| 我想看操逼黄色大片| 黄色大片男人操女人逼| 成年美女黄网站18禁久久| 4个黑人操素人视频网站精品91| 欧洲欧美日韩国产在线| av在线资源中文字幕| 9l人妻人人爽人人爽| 亚洲av第国产精品| 福利视频网久久91| 成年人啪啪视频在线观看| aiss午夜免费视频| 男女第一次视频在线观看| 成人24小时免费视频| 精品suv一区二区69| 天天操天天操天天碰| 国产va精品免费观看| 亚洲图库另类图片区| 欧美中国日韩久久精品| 99re久久这里都是精品视频| 91国内精品自线在拍白富美| 中文字幕无码一区二区免费| 欧美日本在线视频一区| 久久午夜夜伦痒痒想咳嗽P| 91高清成人在线视频| 摧残蹂躏av一二三区| 国产之丝袜脚在线一区二区三区 | 和邻居少妇愉情中文字幕| 久久久久久cao我的性感人妻 | 天堂中文字幕翔田av| 九一传媒制片厂视频在线免费观看| 综合国产成人在线观看| 国产伦精品一区二区三区竹菊| 端庄人妻堕落挣扎沉沦| 欧美久久一区二区伊人| 欧美久久一区二区伊人| 欧美成人小视频在线免费看| 黄色录像鸡巴插进去| 国产精品手机在线看片| 久久久久国产成人精品亚洲午夜| 国产精品久久综合久久| 亚洲国产美女一区二区三区软件| 免费十精品十国产网站| 亚洲欧美综合另类13p| 国产普通话插插视频| 亚洲va欧美va人人爽3p| 日本少妇人妻xxxxx18| 成人精品视频99第一页| 欧美交性又色又爽又黄麻豆| 亚洲成人免费看电影| 国产熟妇乱妇熟色T区| 97人妻无码AV碰碰视频| 色婷婷久久久久swag精品| 亚洲欧美清纯唯美另类| 我想看操逼黄色大片| 亚洲中文字幕人妻一区| 亚洲国产欧美国产综合在线| 视频一区 二区 三区 综合| 天天操天天干天天日狠狠插| 国产免费高清视频视频| 日韩一区二区三区三州| 欧美精品免费aaaaaa| 亚洲欧美自拍另类图片| 久久久久久99国产精品| 老鸭窝日韩精品视频观看| 国产黄色高清资源在线免费观看| 中文字幕 人妻精品| 日韩中文字幕福利av| 激情国产小视频在线| 天天躁夜夜躁日日躁a麻豆| 欧美日韩熟女一区二区三区| 91免费放福利在线观看| 五十路熟女人妻一区二区9933| 亚洲国产免费av一区二区三区 | 国产亚洲欧美另类在线观看| 婷婷久久一区二区字幕网址你懂得| 亚洲 欧美 自拍 偷拍 在线| 天天日天天干天天爱| 欧美亚洲国产成人免费在线| 亚洲精品 欧美日韩| 日韩成人综艺在线播放| 欧美日韩亚洲国产无线码| 国产熟妇一区二区三区av| av日韩在线免费播放| 99热久久这里只有精品| 亚洲一级美女啪啪啪| 内射久久久久综合网| 中文字幕av一区在线观看| 最近的中文字幕在线mv视频| 性感美女诱惑福利视频| 免费在线看的黄网站| 97少妇精品在线观看| 国产无遮挡裸体免费直播视频| 在线 中文字幕 一区| 骚货自慰被发现爆操| 天天日天天操天天摸天天舔| 超污视频在线观看污污污| 欧美精品伦理三区四区| 韩国AV无码不卡在线播放| 欧美精品伦理三区四区 | 亚洲精品 日韩电影| 日本韩国免费福利精品| 久久久久久久精品成人热| sw137 中文字幕 在线| 国产精品3p和黑人大战| 中文字幕熟女人妻久久久| 午夜久久久久久久99| 亚洲一级美女啪啪啪| 亚洲av黄色在线网站| 啊啊好慢点插舔我逼啊啊啊视频| 青青青青青青草国产| 免费在线观看污污视频网站| 国产精品黄页网站视频| 91福利在线视频免费观看| 五十路人妻熟女av一区二区| 天天日天天鲁天天操| 欧美一区二区三区高清不卡tv | 免费一级黄色av网站| 日本免费一级黄色录像| 国产日本欧美亚洲精品视| 日本熟妇色熟妇在线观看| 亚洲成人午夜电影在线观看 | 老司机99精品视频在线观看| 天天干夜夜操天天舔| 亚洲国产精品免费在线观看| 午夜的视频在线观看| 天天操天天射天天操天天天| 九九视频在线精品播放| 五十路熟女人妻一区二区9933| 欧美区一区二区三视频| 欧美久久久久久三级网| 亚洲美女美妇久久字幕组| 自拍偷拍 国产资源| 日本一区精品视频在线观看| 日韩欧美国产精品91| 制服丝袜在线人妻中文字幕| 又色又爽又黄又刺激av网站| 玖玖一区二区在线观看| 国产黄网站在线观看播放| 天天干夜夜操天天舔| 日本少妇在线视频大香蕉在线观看 | 色噜噜噜噜18禁止观看| 国产av自拍偷拍盛宴| 天天日天天摸天天爱| 天天日天天日天天擦| 欧美在线精品一区二区三区视频| AV无码一区二区三区不卡| 欧美一区二区中文字幕电影| 日本av高清免费网站| 少妇被强干到高潮视频在线观看| 综合激情网激情五月天| 国产超码片内射在线| 自拍 日韩 欧美激情| 蜜桃视频17c在线一区二区| 人妻素人精油按摩中出| 适合午夜一个人看的视频| 91麻豆精品91久久久久同性| 亚洲国产香蕉视频在线播放| 国产精品一二三不卡带免费视频| 久久综合老鸭窝色综合久久| 日本熟妇喷水xxx| 日本一区精品视频在线观看| 97欧洲一区二区精品免费| 偷拍自拍 中文字幕| 国产在线91观看免费观看| 老司机你懂得福利视频| 搡老妇人老女人老熟女| 久久久久久97三级| 麻豆性色视频在线观看| 亚洲一级av大片免费观看| 夏目彩春在线中文字幕| 在线观看国产免费麻豆| 女警官打开双腿沦为性奴| 91欧美在线免费观看| 一区二区三区另类在线| 11久久久久久久久久久| 国产精品视频欧美一区二区| 这里只有精品双飞在线播放| 日本熟妇一区二区x x| 欧美成人一二三在线网| 新97超碰在线观看| 亚洲av成人免费网站| 亚洲另类伦春色综合小| 新婚人妻聚会被中出| 一区国内二区日韩三区欧美| 久青青草视频手机在线免费观看| 黄工厂精品视频在线观看 | 91自产国产精品视频| 日韩国产乱码中文字幕| 11久久久久久久久久久| 色在线观看视频免费的| 亚洲免费国产在线日韩| 91九色国产porny蝌蚪| 国产乱子伦精品视频潮优女| 国产熟妇一区二区三区av| 在线国产日韩欧美视频| 国产janese在线播放| 日韩av大胆在线观看| 超碰97人人澡人人| 亚洲综合乱码一区二区| 一区二区三区精品日本| 9色在线视频免费观看| 午夜毛片不卡免费观看视频| 中文字幕日韩精品就在这里| 成人免费公开视频无毒| 揄拍成人国产精品免费看视频| 精品高跟鞋丝袜一区二区| 日本中文字幕一二区视频| 午夜精品在线视频一区| 精品视频一区二区三区四区五区| 青青青青草手机在线视频免费看| 青草久久视频在线观看| 婷婷久久久综合中文字幕| 亚洲精品无码久久久久不卡| 午夜久久香蕉电影网| 青青青青青青青青青青草青青 | 美女张开两腿让男人桶av| 午夜激情精品福利视频| 久久精品国产亚洲精品166m| 欧美viboss性丰满| 欧美中文字幕一区最新网址| 国产精彩对白一区二区三区| 成人蜜桃美臀九一一区二区三区| 只有精品亚洲视频在线观看| 色婷婷精品大在线观看| 宅男噜噜噜666国产| 亚洲va天堂va国产va久| 青青青视频手机在线观看| 青青草亚洲国产精品视频| 大黑人性xxxxbbbb| 18禁网站一区二区三区四区| 日韩欧美国产精品91| 欧美精品中文字幕久久二区| 91免费福利网91麻豆国产精品| 亚洲激情,偷拍视频| 喷水视频在线观看这里只有精品| 青青青青青手机视频| 一区二区视频在线观看免费观看 | 少妇ww搡性bbb91| 一区二区在线观看少妇| 伊人成人综合开心网| 老司机免费福利视频网| 日韩写真福利视频在线观看| 日韩三级黄色片网站| 在线免费观看日本片| 中文字幕日韩精品日本| 久草极品美女视频在线观看| 55夜色66夜色国产精品站| 自拍偷拍亚洲精品第2页| 国内自拍第一页在线观看| 青青青青青青青青青青草青青| 欧亚日韩一区二区三区观看视频| 99精品免费久久久久久久久a| 亚洲精品中文字幕下载| 100%美女蜜桃视频| 99久久中文字幕一本人| 国产精品亚洲在线观看| 男人天堂av天天操| 欧美久久一区二区伊人| 日本精品美女在线观看| 中文字幕一区二区自拍| 开心 色 六月 婷婷| 国产日本精品久久久久久久| 国产高清精品极品美女| 偷拍自拍亚洲视频在线观看| 久久这里有免费精品| 亚洲av无码成人精品区辽| 亚洲国产免费av一区二区三区| 久久久久久久久久一区二区三区 | 在线制服丝袜中文字幕| 80电影天堂网官网| 97香蕉碰碰人妻国产樱花| 激情图片日韩欧美人妻| 在线观看黄色成年人网站| 做爰视频毛片下载蜜桃视频1| 久久一区二区三区人妻欧美| 欧美视频一区免费在线| 亚洲av男人的天堂你懂的| 日本乱人一区二区三区| 国产精品系列在线观看一区二区| 欧美一区二区三区啪啪同性| 2022中文字幕在线| 国产丰满熟女成人视频| 黑人乱偷人妻中文字幕| 亚洲第一黄色在线观看| 亚洲成人午夜电影在线观看| 另类av十亚洲av| av在线播放国产不卡| 韩国一级特黄大片做受| 伊人综合免费在线视频| 亚洲的电影一区二区三区| av天堂中文免费在线| 在线播放国产黄色av| 国产成人午夜精品福利| 做爰视频毛片下载蜜桃视频1| 最后99天全集在线观看| 在线视频免费观看网| 久草福利电影在线观看| 夜色撩人久久7777| 99久久99一区二区三区| 国产高清在线在线视频| 国产成人午夜精品福利| 青青草成人福利电影| 国产高清女主播在线| 亚洲一级av大片免费观看| 欧美乱妇无乱码一区二区| 在线观看黄色成年人网站| 免费观看理论片完整版| 婷婷六月天中文字幕| 国产亚洲四十路五十路| 中文字幕国产专区欧美激情| 黄色片黄色片wyaa| 日韩中文字幕在线播放第二页 | 中文字幕之无码色多多| 99婷婷在线观看视频| 丰满的子国产在线观看| 超黄超污网站在线观看| 啪啪啪啪啪啪啪免费视频| 人妻最新视频在线免费观看| 噜噜色噜噜噜久色超碰| 99精品免费久久久久久久久a| 中文字幕在线欧美精品| 青青草亚洲国产精品视频| av在线shipin| 亚洲精品乱码久久久久久密桃明| 午夜极品美女福利视频| 中国把吊插入阴蒂的视频| 热99re69精品8在线播放| 午夜精品九一唐人麻豆嫩草成人| 天天日天天摸天天爱| 视频在线亚洲一区二区| 爱有来生高清在线中文字幕| 亚洲国产精品黑丝美女| 亚洲欧美成人综合视频| 欧美男人大鸡吧插女人视频 | 唐人色亚洲av嫩草| 91社福利《在线观看| 伊人网中文字幕在线视频| 亚洲一区二区人妻av| 国产视频在线视频播放| 绯色av蜜臀vs少妇| 韩国亚洲欧美超一级在线播放视频 | 亚洲天堂精品久久久| 天天日天天干天天舔天天射| 国产欧美日韩第三页| av天堂加勒比在线| 国产精品黄片免费在线观看| 日本人妻少妇18—xx| 精品av久久久久久久| av老司机亚洲一区二区| 91久久精品色伊人6882| 91麻豆精品91久久久久同性| 77久久久久国产精产品| 久久一区二区三区人妻欧美 | 人妻少妇精品久久久久久| 欧美成人黄片一区二区三区| 超碰在线中文字幕一区二区| 美女小视频网站在线| 1000小视频在线| 超碰在线中文字幕一区二区| 日本一道二三区视频久久 | 美洲精品一二三产区区别| 操日韩美女视频在线免费看| 护士小嫩嫩又紧又爽20p| 欧美国品一二三产区区别| 国产精品久久久久久久久福交| 天天色天天舔天天射天天爽| 中国老熟女偷拍第一页| 亚洲精品色在线观看视频| 欧美专区日韩专区国产专区| 国产一区成人在线观看视频| 国产又粗又猛又爽又黄的视频美国| 日本高清成人一区二区三区| 亚洲精品成人网久久久久久小说| 亚洲人成精品久久久久久久| www日韩毛片av| ka0ri在线视频| 天天日天天干天天舔天天射| 久久久久久久99精品| 春色激情网欧美成人| 中出中文字幕在线观看| 九色porny九色9l自拍视频| 99亚洲美女一区二区三区| 韩国黄色一级二级三级| 人人爱人人妻人人澡39| 国产久久久精品毛片| 2020av天堂网在线观看| 自拍偷拍日韩欧美亚洲| 欧亚日韩一区二区三区观看视频| 精品久久久久久久久久久a√国产 日本女大学生的黄色小视频 | 97a片免费在线观看| 国产一区二区欧美三区| 大尺度激情四射网站| 日本女人一级免费片| 亚洲欧美久久久久久久久| 大香蕉伊人国产在线| 护士特殊服务久久久久久久| 亚洲偷自拍高清视频| 国产普通话插插视频| 亚洲精品无码色午夜福利理论片| 五月色婷婷综合开心网4438| 免费一级黄色av网站| 国产亚洲精品视频合集| av日韩在线观看大全| 75国产综合在线视频| 午夜福利资源综合激情午夜福利资| 国产成人一区二区三区电影网站| 51国产偷自视频在线播放| 天天干天天操天天爽天天摸| 动漫av网站18禁|