[Proyecto] Implementación de un grupo de memoria de alta concurrencia desde cero


Tabla de contenido

1. Introducción del proyecto

1. El prototipo del proyecto.

2. Las tecnologías involucradas en el proyecto y los artículos de referencia anteriores de los blogueros.

3. Tecnología de agrupación

4. Fragmentación interna y externa del grupo de memoria.

2. Veamos primero un diseño de grupo de memoria de longitud fija.

3. Diseño de marco de tres capas de grupo de memoria de alta concurrencia

1. Implementación de caché de subprocesos

1.1 marco general de caché de subprocesos

1.2 Reglas de alineación del mapeo del depósito hash

1.3El almacenamiento local de subprocesos de Thread Local Storage (TLS) logra un acceso sin bloqueos

2. Implementación de caché central

2.1El marco general del caché central

2.2 Gestión de páginas

2.3 Utilice el modo singleton para generar objetos de caché central estáticos globales (modo hambriento)

2.4 Utilice el algoritmo de ajuste de retroalimentación de inicio lento para resolver el problema de asignación de páginas de memoria

2.5 ¿Cómo recuperar objetos de memoria del caché central?

3. Implementación de caché de página.

3.1El marco general del caché de páginas.

3.2 ¿Cómo se aplica el caché central a la memoria del caché de página?

3.3 El hilo se desbloquea cuando la memoria caché central es insuficiente y solicita memoria del caché de la página.

4. Mecanismo de reciclaje de memoria del caché de tres capas.

1. Mecanismo de reciclaje del caché de subprocesos

2. Mecanismo central de reciclaje de caché

2.1 El caché central recicla los objetos de memoria devueltos por el caché de subprocesos.

2.2 El mecanismo por el cual el caché central devuelve páginas de memoria al caché de páginas

3. Mecanismo de reciclaje de caché de página

3.1 El caché de páginas intenta fusionar las páginas anteriores y siguientes para reducir la fragmentación externa.

5. Problema de aplicación de caché de más de 256 KB

6. Utilice un grupo de memoria de longitud fija para separar el proyecto de nuevo/eliminar

7. Análisis de los cuellos de botella de rendimiento de los grupos de memoria de alta concurrencia

8. Utilice el árbol de base en lugar de unordered_map para mejorar el rendimiento del grupo de memoria.

1. Dos árboles de base diferentes

2. ¿Por qué el uso de estructuras de datos como los árboles de base no requiere bloqueo?


Enlace de código del proyecto: Grupo de memoria de alta concurrencia: Proyecto de grupo de memoria de alta concurrencia. Este proyecto extrae la parte central del proyecto de código abierto tcmalloc de Google, que puede mejorar la eficiencia de solicitar y liberar memoria en un entorno de subprocesos múltiples. (gitee.com)

Entorno de adaptación: Windows X86

1. Introducción del proyecto

1. El prototipo del proyecto.

El prototipo de este proyecto es Thread-Caching Malloc, un proyecto de código abierto de Google, que es thread-caching malloc, o tcmalloc para abreviar. Fue escrito por los principales expertos en C ++ de Google en ese momento y era muy conocido: muchas empresas lo utilizaron como optimización del rendimiento y el lenguaje Go lo utilizó directamente como asignador de memoria.

Este proyecto extrae la parte central de tcmalloc y crea un grupo de memoria de alta concurrencia desde cero. En un entorno de subprocesos múltiples , puede reemplazar de manera más eficiente funciones relacionadas con la asignación de memoria del sistema, como malloc y free.

2. Las tecnologías involucradas en el proyecto y los artículos de referencia anteriores de los blogueros.

Idioma: C/C++ (incluido C++11)

[C++11] Explicación detallada de las características comunes de C++11

Estructura de datos: lista enlazada individualmente, lista enlazada circular bidireccional con encabezado, depósito hash

[Estructura de datos] Implementación de lista enlazada individualmente

[Estructura de datos] Implementación de la lista enlazada circular bidireccional líder

[C++] Hash (contenedores asociativos de series desordenadas)

Sistema operativo: gestión de memoria, subprocesos múltiples, bloqueos mutex

[Lenguaje C] realloc, malloc, calloc

[C++] Gestión dinámica de memoria y programación genérica.

【C ++ 11】 Subprocesos múltiples

[Programación del sistema Linux] Creación, espera y terminación de subprocesos múltiples

[Programación del sistema Linux] Exclusión mutua y sincronización de subprocesos múltiples

Patrón de diseño: Patrón Singleton

[C++] Diseño de clase especial + modo singleton

[Programación del sistema Linux] Grupo de subprocesos basado en implementación diferida en modo singleton

3. Tecnología de agrupación

        Cada vez que un programa solicita recursos del sistema operativo, habrá una cierta cantidad de gastos generales. En escenarios donde los recursos se solicitan y liberan con frecuencia, estos gastos generales aumentarán. El uso de la tecnología de agrupación puede reducir esta parte del costo. La tecnología de agrupación significa que el programa solicita un recurso excedente del sistema operativo por adelantado como un "depósito". Cuando el subproceso necesita recursos, va a este grupo de memoria para solicitarlo, lo que Puede resolver el problema de la solicitud frecuente de liberación de pequeños recursos , lo que resulta en una reducción en la eficiencia del sistema y problemas de fragmentación de la memoria . (De hecho, la esencia de la función de biblioteca malloc es también un grupo de memoria).

        En las computadoras, hay muchos lugares donde se usa la tecnología de agrupación: además de los grupos de memoria, también hay grupos de objetos, grupos de conexiones, grupos de subprocesos, etc. Tome el grupo de subprocesos en el servidor como ejemplo: primero inicie un cierto número de Cuando se procesa la solicitud, el hilo vuelve al estado de suspensión.

4. Fragmentación interna y externa del grupo de memoria.

Fragmentación interna: el tamaño de la memoria requerida es menor que el tamaño del bloque de memoria proporcionado por el grupo de memoria, y el exceso de espacio se denomina fragmentación interna;

Fragmentación externa: al solicitar y devolver recursos del grupo de memoria, aunque los recursos restantes en el grupo de memoria son mayores que los recursos requeridos en un momento determinado, la solicitud de espacio falla debido a la discontinuidad del espacio de memoria, esto se llama externo fragmentación.

2. Veamos primero un diseño de grupo de memoria de longitud fija.

        Diseñe un grupo de memoria de longitud fija. _memory apunta a la dirección inicial del grupo de memoria. Cada aplicación obtendrá un bloque de recursos de tipo T. Al mismo tiempo, _memory+=sizeof(T) mueve la dirección para calibrar la dirección inicial de el siguiente recurso de aplicación dirección inicial.

        Al devolver recursos, inserte cada encabezado de recurso devuelto en la lista vinculada _freeList, y los primeros 4/8 bytes de cada bloque de recursos sirven como puntero al siguiente bloque de recursos devuelto.

Interfaz T* New(): esta interfaz se utiliza para solicitar bloques de recursos

1. Si hay bloques de recursos devueltos, los bloques de recursos devueltos se reutilizarán primero. Cabe señalar que cuando el bloque de recursos de longitud fija se asigna al final, es posible que no sea suficiente para un bloque de recursos. En este momento, se cederá el último espacio restante y se abrirá un nuevo grupo de memoria;

2. Cuando solicite un grupo de memoria, use la interfaz VirtualAlloc de Windows y vaya directamente al área del montón para solicitar espacio por página sin malloc (las interfaces similares en Linux incluyen brk y mmap)

3. Es necesario considerar que el tamaño del tipo T puede ser menor que el de un puntero, en este caso la longitud fija de cada bloque de recursos debe ajustarse al tamaño de un puntero (al menos el siguiente puntero debe colocarse, de lo contrario, ¿qué pasará entonces? tapón de cabeza)

4. Después de completar el trabajo de apertura de espacio del bloque de recursos, aún necesita llamar explícitamente al constructor de tipo T, es decir, posición nueva. (¿Por qué no construir el bloque de recursos desde el principio? Debido a que necesitamos solicitar espacio en el grupo de memoria que se ha solicitado, todos los datos de construcción deben escribirse en el bloque de recursos, y si el espacio aún no está asignado , se llama a la construcción, luego ¡Los datos construidos se escribirán en otro lugar!)

Interfaz void Delete (T* obj): esta interfaz se utiliza para limpiar los datos del bloque de recursos devuelto (tenga en cuenta que solo se limpia, no se libera)

1. Para los bloques de recursos devueltos, primero llame al destructor de tipo T para limpiar los recursos. Una vez completada la limpieza, inserte el encabezado del bloque de recursos en la lista vinculada _freeList.

2. ¿No se liberará el grupo de memoria de longitud fija? Si no se libera, el significado del grupo de memoria es para las empresas que solicitan y liberan recursos con frecuencia. Sus bloques de recursos se pueden usar repetidamente y no es una pérdida de memoria. Si el proceso no se detiene y el negocio no se detiene, los recursos del grupo de memoria asignados por el proceso se liberarán naturalmente.

#pragma once
#include <iostream>
#include <exception>
#include <vector>
#include <ctime>

#ifdef _WIN32
	#include <Windows.h>
#else
	//包含linux下brk mmap等头文件
#endif
using std::cout;
using std::endl;
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;
}
//template <size_t N>//N代表每个内存块的大小
//class ObjectPool//定长内存池
//{};
template <class T>//T代表内存对象,每个内存对象的大小是一样的,表示内存块的大小
class ObjectPool//定长内存池
{
public:
	T* New()//内存池单次资源申请
	{
		T* obj = nullptr;
		//申请资源时优先重复利用已归还的资源块
		if (_freeList != nullptr)
		{
			obj = (T*)_freeList;
			_freeList = *(void**)_freeList;
		}
		else
		{
			if (_remainBytes < sizeof(T))//当剩余内存小于一个T对象时,重新开辟一块新内存池
			{
				_remainBytes = 128 * 1024;
				_memory = (char*)SystemAlloc(_remainBytes>>13);//128KB换算成每页8kb,得16页
				if (nullptr == _memory)
				{
					throw std::bad_alloc();
				}
				
			}
			//内存池单次资源申请
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}
		//使用定位new,显式调用T的构造函数初始化构造函数
		new(obj)T;
		return obj;
	}
	void Delete(T* obj)
	{
		obj->~T();//显式调用obj的析构函数,清理对象
		//归还资源块时,进行单链表的头插
		*(void**)obj = _freeList;//找到资源块的头4/8个字节,取决于32位还是64位机器
		_freeList = obj;
	}
private:
	char* _memory = nullptr;//内存池的起始地址
	size_t _remainBytes = 0;//内存池剩余内存
	void* _freeList = nullptr;//指向归还资源块的单链表头指针
};
struct TreeNode
{
	int val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:val(0)
		,_left(nullptr)
		,_right(nullptr)
	{}
};
void TestObjectPool()//测试代码
{
	//申请释放的轮次
	const size_t Rounds = 5;
	//每轮申请释放多少次
	const size_t N = 10000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);
	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();
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	ObjectPool<TreeNode> TNPool;
	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;
}

        El código de prueba compara el grupo de memoria de longitud fija con la palabra clave nueva. Al solicitar nodos de árbol en lotes, el grupo de memoria de longitud fija es más rápido. El grupo de memoria de longitud fija es solo una dirección. Consulte a continuación las ideas de diseño reales de este proyecto:

3. Diseño de marco de tres capas de grupo de memoria de alta concurrencia

        Muchos entornos de desarrollo modernos son de múltiples núcleos y subprocesos. En el escenario de las aplicaciones de memoria, debe haber una feroz competencia de bloqueo. Malloc en sí es realmente muy bueno, por lo que el prototipo de nuestro proyecto tcmalloc es aún mejor en escenarios de alta concurrencia de subprocesos múltiples, por lo que el grupo de memoria de alta concurrencia que implementamos esta vez debe tener en cuenta los siguientes problemas:

1. Problemas de rendimiento. 2. Bloquear el problema de la competencia en un entorno de subprocesos múltiples. 3. Problema de fragmentación de la memoria. 

El grupo de memoria de alta concurrencia consta principalmente de tres partes:

1. Caché de subprocesos: caché de subprocesos, que se utiliza para asignar un uso de memoria inferior a 256 KB. Cada subproceso creado en un proceso tendrá su propia caché de subprocesos independiente. Los subprocesos que soliciten recursos desde aquí no necesitarán bloquearse. Sin embargo, el malloc tradicional bloqueará las aplicaciones de memoria en un entorno de subprocesos múltiples. Esto es alta concurrencia. La base del grupo de memoria eficiencia.

2. Caché central: Caché central. Cuando el caché de subprocesos no es suficiente, el subproceso irá al caché central para solicitar recursos del objeto. Al mismo tiempo, el caché central recuperará los recursos en el momento adecuado para evitar otros. los hilos se queden sin recursos al realizar la solicitud. Hay competencia de recursos en la caché central. Cuando los subprocesos solicitan recursos allí, deben bloquearse y protegerse. Debido a que aquí se utilizan bloqueos de cubo de hash, los bloqueos solo se bloquean cuando se produce un conflicto de hash cuando dos subprocesos solicitan recursos. En segundo lugar, Solo se utilizan cachés de subprocesos, y solo cuando se agotan irán al caché central para solicitar recursos, por lo que la competencia en el caché central no es feroz.

3. Caché de página: Caché de página.Cuando se agota la asignación de objetos de memoria caché central, se asigna una cierta cantidad de memoria del caché de página en unidades de páginas, se corta en bloques de memoria de tamaño fijo y se asigna al caché central. Cuando se recuperan varios objetos de página de intervalo de un intervalo en el caché central, el caché de página recuperará los objetos de intervalo que cumplan las condiciones y fusionará páginas adyacentes para aliviar el problema de la fragmentación de la memoria.

Si el caché de la página no es suficiente, se utilizarán interfaces como VirtualAlloc para solicitar recursos en el área del montón.

1. Implementación de caché de subprocesos

1.1 marco general de caché de subprocesos

        A través del diseño del grupo de memoria de longitud fija, podemos encontrar que el grupo de memoria de longitud fija es mejor que malloc en el caso de longitud fija, pero solo se puede usar para objetos de una longitud, luego podemos diseñar una memoria Piscina con múltiples longitudes fijas. Hay opciones largas disponibles según sea necesario.

        El caché de subprocesos es una estructura de depósito de hash. La tabla hash se asigna de acuerdo con el tamaño del objeto de bloque de memoria. Cada depósito de hash monta un bloque de memoria del tamaño actual del conjunto de depósitos de hash. Cada subproceso tiene un objeto de caché de subprocesos, por lo que no es necesario bloquear al solicitar recursos aquí.

        Al solicitar recursos, proporcione bloques de recursos en función de un espacio mayor o igual a la demanda. Por ejemplo, solo necesito 10 bytes y usted solo puede darme 16 bytes. No hay manera, los 6 bytes adicionales se convertirán en fragmentos internos. . Una vez que se devuelve el bloque de recursos, el bloque de recursos se volverá a insertar en el depósito hash correspondiente.

1.2 Reglas de alineación del mapeo del depósito hash

        El caché de subprocesos tiene un espacio total de 256 KB. Es imposible para nosotros asignar cada valor de longitud fija a la tabla hash. Solo podemos usar ciertas reglas para asignar un rango de valores al mismo depósito.

        Entonces, ¿cómo determinar este rango? Hay un espacio total de 256 KB. Si el intervalo de alineación es demasiado largo, fácilmente provocará fragmentación interna. Si es demasiado corto, provocará demasiados depósitos de hash.

        En primer lugar, el primer depósito debe tener 8 bytes, porque el tamaño del puntero en una máquina de 64 bits es de 8 bytes, ¡y el tamaño del bloque de memoria debe al menos acomodar el puntero! Ahora que la longitud inicial está disponible, ¿cómo se debe determinar el intervalo de alineación?

Control global de residuos de escombros internos a alrededor del 10% como máximo.

[1,128] Lista libre alineada de 8 bytes [0,16)

[128+1,1024] Lista libre alineada de 16 bytes[16,72)

[1024+1,8*1024] Lista libre de alineación de 128 bytes[72,128)

[8*1024+1,64*1024] Lista libre alineada de 1024 bytes [128,184)

[64*1024+1,256*1024] Lista libre de alineación de 8*1024 bytes [184,208)

        Explicación: El requisito de memoria es de 129 bytes. De acuerdo con las reglas anteriores, el depósito adaptado a 129 bytes es 128 + 16 = 144 bytes; el grupo de memoria me da un bloque de memoria de 144 bytes y la relación de fragmentación interna = (144- 129)/144=10,4%.

En este momento, se requiere un bloque de recursos externo de bytes bytes externamente, y el tamaño del bloque de memoria         después de que el bloque de recursos está alineado hacia arriba se genera a través del siguiente código (la subfunción de comentario está escrita por personas normales y la resaltada La subfunción está escrita por expertos en tcmalloc):

/*size_t _RoundUp(size_t size, size_t alignNum)
{
    size_t alignSize = 0;
    if (size % alignNum != 0)
    {
        alignSize = ((size / alignNum) + 1) * alignNum;
    }
    else
        alignSize = size;
    return alignSize;
}*/
static inline size_t _RoundUp(size_t bytes, size_t alignNum)//bytes:需要申请的字节;alignNum:对齐数
{
    return (bytes + alignNum - 1) & ~(alignNum - 1);
}
//用于返回申请内存对应的内存块
static inline size_t RoundUp(size_t bytes)
{
    if (bytes <= 128)
    {
        return _RoundUp(bytes, 128);
    }
    else if (bytes <= 1024)
    {
        return _RoundUp(bytes, 1024);
    }
    else if (bytes <= 8 * 1024)
    {
        return _RoundUp(bytes, 8*1024);
    }
    else if (bytes <= 64 * 1024)
    {
        return _RoundUp(bytes, 64*1024);
    }
    else if (bytes <= 256 * 1024)
    {
        return _RoundUp(bytes, 256*1024);
    }
    else
    {
        assert(false);
        return -1;
    }
}

Utilice el siguiente código para encontrar en qué depósito se encuentra el bloque de recursos después de la alineación hacia arriba del bloque de recursos :

//static inline size_t _Index(size_t bytes,size_t alignNum)//bytes:需要申请的字节;alignNum:对齐数
//{
//	if (bytes % alignNum == 0)
//	{
//		return bytes / alignNum - 1;
//	}
//	else
//		return bytes / alignNum;
//}
static inline size_t _Index(size_t bytes,size_t align_shift)//bytes:需要申请的字节-上一个区间的大小;align_shift:对齐数的移位值
{
    return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
// 计算映射的哪一个自由链表桶
static inline size_t Index(size_t bytes)//bytes:需要申请的字节
{
    assert(bytes <= MAX_BYTES);
    // 每个区间有多少个链
    static int group_array[4] = { 16, 56, 56, 56 };
    if (bytes <= 128) {
        return _Index(bytes, 3);
    }
    else if (bytes <= 1024) {
        return _Index(bytes - 128, 4) + group_array[0];
    }
    else if (bytes <= 8 * 1024) {
        return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
    }
    else if (bytes <= 64 * 1024) {
        return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1]
            + group_array[0];
    }
    else if (bytes <= 256 * 1024) {
        return _Index(bytes - 64 * 1024, 13) + group_array[3] +
            group_array[2] + group_array[1] + group_array[0];
    }
    else {
        assert(false);
    }
    return -1;
}

1.3El almacenamiento local de subprocesos de Thread Local Storage (TLS) logra un acceso sin bloqueos

class ThreadCache
{
public:
	//申请和释放对象
	void* Allocate(size_t size);
	void Deallocate(void* ptr, size_t size);
	// 从中央缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t size);//index:计算位于哪一个桶;size:对齐之后的内存块大小
private:
	FreeList _freeList[NFREE_LIST];//挂载208个哈希桶的哈希表
};

//每个线程都有一份自己的pTLSThreadCache
static _declspec(thread)ThreadCache* pTLSThreadCache = nullptr;//pTLSThreadCache:指向Threadcache对象的指针

        Este código utiliza la palabra clave __declspec(thread) de Microsoft Visual C++ para especificar que la variable es una variable de almacenamiento local de subproceso (Thread Local Storage, TLS). Esto significa que cada hilo tendrá una copia de la variable, y solo ese hilo podrá acceder y modificar la variable y no será compartida por otros hilos. Cada subproceso tiene su propia instancia de ThreadCache, por lo que puede evitar problemas de competencia y sincronización entre subprocesos y mejorar la concurrencia y el rendimiento del programa.

        La implementación de interfaz específica de ThreadCache. Tenga en cuenta que si el caché de subprocesos de un determinado subproceso no es suficiente, solicitará memoria al caché central.

void* ThreadCache::Allocate(size_t bytes)//申请对象
{
	assert(bytes <= MAX_BYTES);
	size_t alignNum = SizeClass::RoundUp(bytes);//对齐之后的内存块大小
	size_t index = SizeClass::Index(bytes);//计算位于哪一个桶
	if (!_freeList[index].Empty())//去对应的哈希桶中的自由链表中申请资源块
	{
		return _freeList[index].Pop();
	}
	else//如果对应的自由链表已经没有资源块了,那就要去中央缓存申请资源了
	{
		return FetchFromCentralCache(index, alignNum);//index:计算位于哪一个桶;size:对齐之后的内存块大小
	}
}
void ThreadCache::Deallocate(void* ptr, size_t bytes)//释放对象
{
	assert(ptr);
	assert(bytes <= MAX_BYTES);
	size_t index = SizeClass::Index(bytes);//算出位于几号桶
	//头插
	_freeList[index].Push(ptr);
}

Utilice las siguientes dos interfaces de encapsulación para implementar los bytes externos requeridos entrantes, devolver y destruir los bloques de recursos correspondientes.

//加上static,防止该头文件被源文件重复包含导致函数重定义
static void* ConcurrentAlloc(size_t size)//线程通过该函数去各自的线程缓存申请内存块
{
	if (nullptr == pTLSThreadCache)//这里不用加锁,每个线程独有一份pTLSThreadCache
	{
		pTLSThreadCache = new ThreadCache;
	}
	//cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;
	return pTLSThreadCache->Allocate(size);
}
static void ConcurrentFree(void* ptr,size_t size)//外部调用该函数,释放内存块
{
	assert(pTLSThreadCache);
	pTLSThreadCache->Deallocate(ptr, size);
}

2. Implementación de caché central

2.1El marco general del caché central

        El caché central es muy similar al caché de subprocesos. También es una estructura de depósito de hash que se asigna de acuerdo con las condiciones de las reglas. Ambos tienen 208 depósitos de hash. La ubicación del depósito correspondiente del bloque de memoria en el caché de subprocesos y el caché central el caché es el mismo. la diferencia es:

1. El depósito de hachís en esta área debe estar bloqueado y protegido, y el candado utilizado es un candado de depósito (cada depósito de hachís tendrá un candado para garantizar la seguridad del hilo)

2. El depósito de hash del caché central debe diseñarse como una lista doblemente enlazada principal, porque después de que Span recupera la página, debe devolverse al caché de páginas de la siguiente capa, que debe satisfacer la inserción y eliminación en Cualquier posición.

3. El caché central debe configurarse en modo singleton, mientras que cada variable en el caché de subprocesos es única

4. Lo que está montado en el depósito de hash en esta área no se corta en pequeños bloques de objetos, sino en cada tramo (un gran bloque de memoria en páginas). El número de páginas asignadas al tramo de diferentes depósitos de hash será diferente .Diferente), lo que está montado debajo de Span es el objeto real cortado.

5. Los bloques de recursos solicitados por el caché de subprocesos desde el caché central se insertarán en el caché de subprocesos cuando se devuelvan. Cuando un subproceso solicita con frecuencia bloques de recursos del caché central, todos se guardarán en su "bolsillo". cuando se lanzan, lo que hará que la caché de subprocesos crezca cada vez más. Por lo tanto, una vez liberados todos los bloques de memoria prestados, la caché de subprocesos debe devolver los objetos de recursos solicitados en ese momento página por página. La caché central puede recuperar estas memorias y entregárselas a otros subprocesos que necesitan memoria para lograr una programación equilibrada.

6. Entonces, ¿cómo sabe el caché central que el caché de subprocesos del bloque de memoria "prestado" se ha agotado y se ha recuperado? Puede agregar una nueva variable miembro en la clase para registrar cuántos objetos de memoria se "prestan" y cada vez que se recupera uno, --, igual a 0, se puede iniciar el reciclaje.

7. De manera similar, cuando la memoria caché central es insuficiente, solicitará espacio a la siguiente capa y, después de que la siguiente capa recupere las páginas, continuará consolidándose para reducir la fragmentación de la memoria (fragmentación externa).

2.2 Gestión de páginas

        Podemos definir una clase Span para administrar grandes bloques de memoria con múltiples páginas consecutivas. Luego, estas páginas necesitan un número (similar a una dirección). Puede definir una variable miembro _spanId en la clase para representar el número de página.

        Por ejemplo, una máquina de 32 bits se puede dividir en 2^32/2^13=2^19 páginas, pero el número de páginas en una máquina de 64 bits se duplica directamente exponencialmente a 2^64/2^13=2 ^ 51 páginas. Para resolver el problema de 64 bits, si la máquina tiene demasiadas páginas (para resolver el problema del tamaño de número de página insuficiente), puede utilizar la compilación condicional para distinguir. Tenga en cuenta que win64 tiene macros win64 y win32. definiciones, por lo que primero debe determinar si _WIN64 existe en la máquina actual:

#ifdef _WIN64
	typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#elif
	//Linux等平台
#endif

//管理多个连续页的大块内存结构(带头双向循环链表的节点——页节点)
struct Span
{
	PAGE_ID _pageId = 0;//页号,类似地址。32位程序2^32,每一页8K,即2^32/2^13=2^19页;64位会有2^51页。
	size_t _n = 0;//页数
	//带头双向循环链表
	Span* _next = nullptr;//记录上一个页的地址
	Span* _prev = nullptr;//记录下一个页的地址
	size_t _useCount = 0;//已经“借给”Thread Cache的小块内存的计数
	void* _freeList = nullptr;//未被分配的切好的小块内存自由单链表,挂载在页下
};
//页的带头双向循环链表(带桶锁)
class SpanList
{
	SpanList()
	{
		_head = new Span;
		assert(_head);
		_head->_prev = _head;
		_head->_next = _head;
	}
	void Insert(Span* pos, Span* newSpan);//在pos位置之前插入
	void Earse(Span* pos);
private:
	Span* _head;//哨兵位
	std::mutex _mtx;//桶锁
};

2.3 Utilice el modo singleton para generar objetos de caché central estáticos globales (modo hambriento)

        En el caché de subprocesos, utilizamos TLS para implementar un caché de subprocesos único para cada subproceso, lo que le permite lograr un acceso sin bloqueos en esta área. El caché central necesita usar bloqueos de depósito para garantizar la seguridad de los subprocesos, por lo que todos los subprocesos deben poder acceder al mismo caché central. ¿No es este el modo singleton, que genera un objeto global estático al que pueden acceder todos los subprocesos?

//单例饿汉模式
class CentralCache
{
public:
	//使用偷家函数获取静态单例对象,加static是因为静态方法无需对象即可调用
	static CentralCache* GetInStance()
	{
		return &_sInst;
	}
private:
	SpanList _spanLists[NFREE_LIST];//208个桶
private:
	static CentralCache _sInst;//静态单例对象(不要在.h文件定义,因为源文件包含了该头文件即可看到单例对象)
	//构造函数私有+禁用拷贝构造和赋值
	CentralCache(const CentralCache&) = delete;
	CentralCache& operator=(const CentralCache&) = delete;
};
CentralCache CentralCache::_sInst;//单例对象的定义(在源文件定义)

2.4 Utilice el algoritmo de ajuste de retroalimentación de inicio lento para resolver el problema de asignación de páginas de memoria

Cuando un caché de subprocesos realiza una solicitud de memoria al caché central, hay dos cuestiones que requieren atención:

1. ¿Cuántas páginas de memoria debe darle el caché central cada vez? Si das demasiado, los recursos quedarán inactivos; si das muy poco, la frecuencia de aplicación de subprocesos aumentará, la probabilidad de conflictos de acceso aumentará y la eficiencia disminuirá.

2. El caché de subprocesos se aplica a objetos de memoria de diferentes tamaños ¿Cuáles son las reglas de asignación del caché central? Por ejemplo, si el caché de subprocesos requiere un bloque de memoria de 8 bytes y el caché de subprocesos requiere un bloque de memoria de 256 KB, la cantidad de páginas asignadas por el caché central es definitivamente diferente.

        El número de objetos de memoria asignados se determina en función del tamaño del objeto de memoria que debe solicitar el caché de subprocesos. A los objetos pequeños se les asignan más puntos (límite superior de control) y a los objetos grandes se les asignan menos puntos.

// thread cache一次从central cache获取多少个对象
static size_t NumMoveSize(size_t size)//size:单个对象的大小
{
    assert(size > 0);
    // [2, 512],一次批量移动多少个对象的(慢启动)上限值
    // 小对象一次批量上限低
    int num = MAX_BYTES / size;//256KB/size
    if (num < 2)//申请256KB的大对象,分配2个对象
        num = 2;
    if (num > 512)//小对象,num大于512,仅分配512个对象
        num = 512;
    return num;
}

        Si solicita un objeto de memoria de 8 bytes, de acuerdo con la lógica del código anterior, el caché central asignará 512 objetos de 8 bytes cada vez. Si el caché de subprocesos solo necesita usar unos pocos, habrá muchos inactivos. Por lo tanto, es necesario agregar la siguiente lógica para lograr la asignación de crecimiento lento de objetos de memoria de diferentes tamaños (nueva variable miembro size_t MaxSize = 1 en la clase _freeList):

void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	//慢开始调节算法
	size_t batchNum = std::min(_freeList[index].MaxSize(),SizeClass::NumMoveSize(size));
	if (batchNum == _freeList[index].MaxSize())
	{
		_freeList[index].MaxSize() += 1;
	}
	return nullptr;
}

2.5 ¿Cómo recuperar objetos de memoria del caché central?

A continuación se muestran algunas operaciones básicas de listas vinculadas:

size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size); //计算线程申请的内存对象向上对齐后位于哪一个桶
	_spanLists[index]._mtx.lock();//线程进来先上锁
	Span* span = CentralCache::GetOneSpan(_spanLists[index], size);//获取非空的页
	assert(span);
	assert(span->_freeList);
	end = start;
	for (size_t i = 0; i < batchNum - 1; ++i)
	{
		end = NextObj(end);
	}
	span->_freeList= NextObj(end);
	NextObj(end) = nullptr;
	_spanLists[index]._mtx.unlock();
	return batchNum;
}

        Sin embargo, cuando la cantidad de objetos que deben recuperarse es mayor que la cantidad de objetos que quedan en la página actual, el código escrito anteriormente provocará un bloqueo de desreferencia de puntero nulo/fuera de límites. Por ejemplo, en el caso 2, en este caso, hay que controlar el final para que no llegue vacío, y quitar tantos objetos como queden en la página actual.

/**
  * @brief  从中央缓存获取一定数量的对象给thread cache
  * @param  start:申请对象的起始地址
  * @param  end:申请对象的结束地址
  * @param  batchNum:通过调节算法得出的中央缓存应该给线程缓存的对象个数
  * @param  size:线程缓存申请的单个对象大小
  * @retval 中央缓存实际给的对象个数,因为当前页的资源对象可能不足了。
  */
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size); //计算线程申请的内存对象向上对齐后位于哪一个桶
	_spanLists[index]._mtx.lock();//线程进来先上锁
	Span* span = CentralCache::GetOneSpan(_spanLists[index], size);//获取非空的页
	//从该非空页中拿走对象交给threan cache
	assert(span);
	assert(span->_freeList);
	end = start;
	size_t i = 0;
	while (i < batchNum - 1 && NextObj(end) != nullptr)
	{
		++i;
		end = NextObj(end);
	}
	span->_freeList= NextObj(end);
	NextObj(end) = nullptr;
	_spanLists[index]._mtx.unlock();
	return i + 1;
}

3. Implementación de caché de página.

3.1El marco general del caché de páginas.

        Todavía existen algunas diferencias entre la caché de página, la caché de subprocesos y la caché central. El depósito de hash del caché de páginas se divide en unidades de páginas. Este es un mapeo del método de direccionamiento directo. Cuántas páginas de caché necesita el caché central, vaya al depósito de hash del tamaño de página correspondiente en el caché de páginas. Las páginas en el depósito de hash de caché de páginas están conectadas mediante una lista enlazada circular bidireccional con encabezado y no se cortarán en objetos pequeños.

Respuesta de detalles de caché de página:

1. La lógica del caché de páginas.

Si el caché central necesita dos páginas de objetos, pero el caché de páginas acaba de consumir las dos páginas de objetos, entonces encontrará un depósito hash con más de dos páginas de páginas, las dividirá y entregará las dos páginas de memoria a el caché central.La memoria restante se montará en el depósito de hash mapeado por la memoria restante. Si mira hacia arriba, todos los depósitos de hash están vacíos. En este momento, el caché de la página solicitará al área del montón un bloque grande de 128 páginas de memoria para asignar.

2. ¿Por qué la caché de la página debería diseñarse en modo singleton?

El caché de página y el caché de subprocesos no son como el caché de subprocesos, que usa TLS para permitir que cada subproceso ocupe un caché de subprocesos exclusivo, sino que tiene una copia global única para que cada subproceso pueda bloquear y acceder, por lo que debe diseñarse. en modo singleton. (También usando el modo hombre hambriento)

3. ¿Por qué la página más grande está configurada en 128 páginas?

Esto se debe a que el objeto de memoria más grande en el caché central y el caché de subprocesos tiene solo 256 KB. Según el diseño del código de nivel superior, es posible solicitar 2 de estos objetos de memoria a la vez, con un total de 512 KB. Luego, se calculan 128 páginas en base a 4 bytes por página en una plataforma de 32 bits son exactamente 512 bytes, una combinación perfecta. (Por supuesto, el tamaño de 128 páginas en un entorno de 64 bits es 1 M, que es más que suficiente)

4. ¿Por qué la caché de la página no puede utilizar bloqueos de depósito como la caché central? ¿Pero sólo se puede bloquear en su totalidad?

Puede utilizar una cerradura de barril, pero la eficiencia se reducirá. Como se mencionó en el primer punto, cuando el hilo descubre que el objeto en el depósito de destino ya no está, buscará un objeto de memoria más grande en el caché de la página. Para decirlo sin rodeos, recorre hacia arriba qué depósito no está vacío. Cuando se utiliza un bloqueo de cubo, se producirá un cierto fenómeno durante el recorrido: un hilo con frecuencia solicita y libera bloqueos, lo que ralentiza la velocidad de recorrido del hilo. Por lo tanto, es necesario bloquear la caché de la página en su totalidad.

5. ¿Cómo realizar el reciclaje de memoria?

Si el useCount del intervalo en el caché central es igual a 0, significa que se han devuelto todos los pequeños bloques de memoria asignados al caché de subprocesos, y el caché central devuelve el intervalo al caché de la página. número para comprobar si las páginas adyacentes están libres. Si están libres, fusionarlas para resolver el problema de fragmentación de la memoria.

3.2 ¿Cómo se aplica el caché central a la memoria del caché de página?

1. Proceso de solicitud de memoria del hilo.

        Como se mencionó anteriormente, cuando un subproceso descubre que la memoria caché del subproceso es insuficiente, utilizará el algoritmo de ajuste de retroalimentación de inicio lento para solicitar memoria del caché central. Si el objeto de memoria en el depósito hash correspondiente al caché central está vacío, la memoria se aplicará al caché de la página. La forma en que el caché de página asigna memoria está relacionada con el algoritmo de retroalimentación de inicio lento. Por ejemplo, el caché de subprocesos solicita un objeto de 8 bytes, obtiene la cantidad de 2 objetos que deben asignarse a través del algoritmo de ajuste de retroalimentación de inicio lento y luego multiplica 8 bytes para obtener 16 palabras y luego muévela hacia la derecha en 13 bits (equivalente a dividir por 8 KB). Si la última página tiene menos de una página, se contará como una página.

/**
  * @brief  central cache向page cache申请内存,page cache分配内存算法
  * @param  size:单个对象的大小
  * @retval 返回申请的页数
  */
static size_t NumMovePage(size_t size)
{
    size_t num = NumMoveSize(size);//返回线程缓存单次从中央缓存获取多少个对象
    size_t npage = num * size;
    npage >>= PAGE_SHIFT;//右移13位,相当于除等8KB
    if (npage == 0)
        npage = 1;
    return npage;
}

2. Memoria dividida

1. Retroceda la dirección virtual de la dirección inicial de la página según el número de página

2. Divida la página obtenida en objetos pequeños e inserte el final del objeto en la lista enlazada libre correspondiente del caché central.

        Al insertar, debe asegurarse de que la memoria de cada objeto pequeño siga siendo continua después de la inserción, lo que permite que el caché de subprocesos garantice la continuidad de la memoria y mejore el uso de la memoria al solicitar el uso de la memoria. Método de inserción: Primero tome una pieza como cabeza e inserte la cola una a la vez de izquierda a derecha.

3. Obtenga el lapso de k páginas del caché de páginas.

¿Ves NewSpan en la captura de pantalla de arriba? Es el método utilizado para obtener K páginas.

        Si desea obtener el objeto de 3 páginas del caché de la página, puede ir directamente a 3page para obtener una pieza, eliminar el encabezado y eliminarlo. Si desea un objeto de 2 páginas, pero no hay ningún objeto en el depósito de hash de 2 páginas en la imagen, debe ir a 3 páginas para obtener un fragmento de memoria, de las cuales 2 páginas se entregan al hilo y la 1 página restante se cuelga. el depósito de hash de 1 página. Si continúa buscando y no hay memoria en todos los depósitos de hash, entonces el caché de página solicitará 128 páginas de memoria en el montón y se llamará a sí mismo de forma recursiva para completar la lógica de segmentación y suspensión del objeto de 128 páginas.

/**
  * @brief  从page cache获取k页的span
  * @param  k:页数
  * @retval 返回获取到的span的地址
  */
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//先看一下映射桶有没有对象,有的话直接拿走
	if (!_spanLists[k].Empty())
	{
		return _spanLists->PopFront();
	}
	//检查一下后面的哈希桶里有没有span
	for (auto i = k+1; i < NPAGES; ++i)
	{
		if (!_spanLists[i].Empty())
		{
			//拿出来切分,切分成k页和i-k页,k页的span给central cache,i-k页的span挂到i-k的哈希桶
			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;
		}
	}
	//找遍了还是没有,这时,page cache就要找堆要一块128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;
	//将bigSpan插入到对应的桶中
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	return PageCache::NewSpan(k);//递归调用自己,让下一个栈帧处理切分、挂起逻辑
}

3.3 El hilo se desbloquea cuando la memoria caché central es insuficiente y solicita memoria del caché de la página.

4. Mecanismo de reciclaje de memoria del caché de tres capas.

1. Mecanismo de reciclaje del caché de subprocesos

        Cuando la longitud de la lista vinculada en el depósito de hash en la caché de subprocesos es mayor que la memoria solicitada en un lote, comienza el reciclaje. (Como se mencionó anteriormente, la memoria solicitada por el caché de subprocesos en un lote aumentará mediante el algoritmo de ajuste de retroalimentación de inicio lento. A medida que aumenta la cantidad de veces que el depósito solicita memoria del caché central, la longitud de la siguiente lista de reciclaje será más extenso.)

2. Mecanismo central de reciclaje de caché

2.1 El caché central recicla los objetos de memoria devueltos por el caché de subprocesos.

        Las direcciones de los objetos de memoria reciclados por el caché de subprocesos no son consecutivas. Sus direcciones pueden estar ubicadas en diferentes tramos del mismo depósito de hash en el caché central. Entonces, ¿cómo montar correctamente cada memoria en el tramo apropiado? Cree un unordered_map<PAGE_ID,Span*> para que el ID de la página donde se encuentra el objeto de memoria corresponda al puntero del depósito, para encontrar la ubicación de montaje. Vea el ejemplo a continuación:

        En este momento, algunos amigos pueden preguntar, si el caché de subprocesos devuelve el objeto, simplemente recorra directamente la ID de la página a la que pertenece el objeto devuelto y luego recorra las _sapnLists del depósito de hash correspondiente al caché central para encontrar el intervalo correspondiente a el objeto de memoria... Esta idea es realmente simple. Sin embargo, la complejidad del tiempo es O ( n^2), lo cual es un poco conmovedor, por lo que se agrega un nuevo contenedor unordered_map para establecer la relación de mapeo entre los objetos y el intervalo *. La complejidad del tiempo de búsqueda de unordered_map es O (1), y la complejidad del tiempo de atravesar objetos de memoria es O (N), por lo que el contenedor unordered_map se usa aquí para reducir la complejidad del tiempo a O (N).

2.2 El mecanismo por el cual el caché central devuelve páginas de memoria al caché de páginas

        Cuando el _useCount del caché central (el número de objetos de memoria prestados al caché de subprocesos) es igual a 0, significa que se han devuelto todos los objetos de memoria prestados al caché de subprocesos en el lapso actual. En este momento, ReleaseSpanToPageCache Se debe llamar a la función de caché de la página para realizar la reubicación de la página. El código de la siguiente figura implementa el código descrito en 2.1 y 2.2 de esta sección:

3. Mecanismo de reciclaje de caché de página

3.1 El caché de páginas intenta fusionar las páginas anteriores y siguientes para reducir la fragmentación externa.

        El gran bloque de memoria originalmente asignado al caché central por el caché de página se divide en bloques pequeños por el caché central y el caché de subprocesos lo solicita. De esta manera, la dirección inicial del bloque de memoria se ha estropeado. Cuando el El caché de la página recibe estos bloques más pequeños de memoria nuevamente, bloquee, debe intentar fusionar las páginas anteriores y siguientes para reducir el problema de la fragmentación externa.

        Solo los objetos de memoria ubicados en el caché de la página pueden iniciar la fusión. Entonces, ¿cómo distinguir si un intervalo se encuentra actualmente en el caché de la página? Hay una variable _useCount en la clase Span. No crea que si esta variable es 0, significa que la memoria está ubicada en el caché de la página, porque es posible que el caché central haya solicitado un gran bloque de memoria de la página. caché y aún no lo ha asignado._useCount es igual a 0. En este momento, si Cuando el caché de la página inicia una recuperación, pueden surgir problemas. La solución correcta es agregar una variable miembro bool _isUse=false; en la clase Span. Si span se asigna al caché central, esta variable miembro se modificará a verdadero. Si esta variable es falsa, demuestra que está en el caché de la página y se puede fusionar.

        Por ejemplo, el ID de página de span es 2000 y actualmente hay 6 páginas consecutivas de memoria en span, luego debe continuar buscando si existen páginas adyacentes, como se muestra en los dos ejemplos de la figura:

        Al fusionar, el tramo base se toma como centro y los identificadores de los tramos circundantes se juzgan desde ambos lados para ver si están en un estado reciclable. Este paso se repite para fusionar ambos lados hasta que los identificadores en ambos lados de el intervalo está en un estado que no se puede fusionar o el intervalo alcanza un máximo de 128 páginas, o no se puede encontrar la correspondencia entre la identificación y el intervalo en el mapa hash (significa que el sistema operativo no asigna la identificación del intervalo a la memoria caché de la página). Y no pertenece a la memoria asignada por el sistema operativo al grupo de memoria. Naturalmente, no existe una relación correspondiente en el hash).

/**
  * @brief  页缓存回收中央缓存的span,并合并相邻的span
  * @param  span:span的地址
  * @retval 无返回值
  */
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//对span前后的页,尝试进行合并
	//向前合并
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;//先判断一下前一个span是否可以被回收
		auto ret = _idSpanMap.find(prevId);
		if (ret == _idSpanMap.end())//说明ret位置的span不可被回收
		{
			break;
		}
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)//说明该span正在中央缓存,无法回收
		{
			break;
		}
		if (prevSpan->_n + span->_n > NPAGES-1)//超过108页
		{
			break;
		}
		//进行合并
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;
		_spanLists[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}
	//向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId +span->_n;//先判断一下前一个span是否可以被回收
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())//说明ret位置的span不可被回收
		{
			break;
		}
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)//说明该span正在中央缓存,无法回收
		{
			break;
		}
		if (nextSpan->_n + span->_n > NPAGES - 1)//超过108页
		{
			break;
		}
		//进行合并
		span->_n += nextSpan->_n;
		_spanLists[nextSpan->_n].Erase(nextSpan);
		delete nextSpan;
	} 
	//走到这里,将span头插至对应页缓存中的哈希桶中
	_spanLists[span->_n].PushFront(span);//
	span->_isUse = false;//确保回收的大块内存可回收
	_idSpanMap[span->_pageId] = span;//建立映射
	_idSpanMap[span->_pageId+span->_n-1] = span;//建立映射
}

5. Problema de aplicación de caché de más de 256 KB

        Si un subproceso solicita un caché de menos de 256 K a la vez, debe seguir el proceso normal, es decir, solicitar memoria del caché de tres capas;

        Si la solicitud única de un hilo para un objeto es mayor o igual a 32 páginas y menor o igual a 128 páginas, es decir, cuando 256 KB <= tamaño de la aplicación <= 1024 KB, puede solicitar memoria directamente desde el caché de la página;

        Si la solicitud única de un hilo para un objeto tiene más de 128 páginas, es decir, más de 1024 KB. El hilo solicitará directamente memoria del área del montón.

6. Utilice un grupo de memoria de longitud fija para separar el proyecto de nuevo/eliminar

        pageCache.cpp usa new y delete en muchos lugares. Este artículo ya ha escrito sobre el diseño de un grupo de memoria de longitud fija en la segunda sección. En este proyecto, tanto lo nuevo como lo eliminado están en unidades de extensión, lo que se ajusta perfectamente al diseño de un grupo de memoria de longitud fija. Al mismo tiempo, la segunda sección de este artículo ha demostrado que la velocidad de solicitud de espacio en el grupo de memoria de longitud fija es mucho más rápida que la velocidad de la nueva, por lo que es muy apropiado utilizar la interfaz de solicitud y liberación. memoria en el grupo de memoria de longitud fija aquí.

        Además, se usa unordered_map de STL. Al establecer la relación de mapeo, se usa new internamente y luego se usa el árbol de base para separarse de unordered_map.

7. Análisis de los cuellos de botella de rendimiento de los grupos de memoria de alta concurrencia

        Se puede ver que el grupo de memoria en este momento no es tan rápido como malloc y no es gratuito en la biblioteca para solicitar y liberar memoria en un entorno de subprocesos múltiples. A través del análisis del perfilador de rendimiento que viene con Visual Studio, se puede ver que la mayor parte del consumo de rendimiento proviene del bloqueo, desbloqueo y búsqueda de unordered_map<PAGE_ID, Span*> _idSpanMap.

Por lo tanto, el árbol de base se utiliza para reemplazar unordered_map para optimizar el rendimiento.

8. Utilice el árbol de base en lugar de unordered_map para mejorar el rendimiento del grupo de memoria.

1. Dos árboles de base diferentes

Utilice árboles de base de uno y dos niveles para optimizar el rendimiento:

        El primer nivel utiliza el método de direccionamiento directo y establece una relación de mapeo uno a uno entre todos los identificadores y el intervalo* en un entorno de 32 bits. Los bytes de espacio que deben asignarse =2^(19)*4= 2 M. Pero no es adecuado en un entorno de 64 bits, porque el espacio que se debe abrir en un entorno de 64 bits =2^(51)*8es de 4 TB.

        El segundo nivel ocupa exactamente el mismo espacio que el árbol de base del primer nivel y tampoco es adecuado para máquinas de 64 bits. Cuando llega un ID, los 19 bits inferiores de este ID de tipo int son ID válidos (los bits superiores son todos 0). Los 5 bits superiores de estos 19 bits determinan en qué bit de la primera capa estará el ID. Entre estos 19 bits Los 14 bits inferiores determinarán en qué bit de la segunda capa se encuentra. Para cada identificación, existe una ubicación de mapeo única. En el proyecto, puede elegir uno de los árboles de base de primer y segundo nivel.

2. ¿Por qué el uso de estructuras de datos como los árboles de base no requiere bloqueo?

        ¡Primero hablemos de por qué es necesario bloquear unordered_map! El mapa_ordenado de este proyecto se usa para establecer la relación de mapeo entre PAGE_ID y Span*, y el hash se usa para buscar y agregar la relación de mapeo.

donde la búsqueda se utiliza para:

1. Al liberar el objeto, es necesario encontrar la relación de mapeo. (Gratis Simultáneo)

2. Los objetos de memoria devueltos por el caché de subprocesos pueden no estar en el mismo depósito de hash del caché central. Cada objeto de memoria devuelto debe llamar a una búsqueda para confirmar a qué depósito de hash del caché central debe devolverse. (ReleaseListToSpans)

La relación de mapeo se agrega para: (se mapeará la memoria eliminada del caché de la página)

1. Todos los objetos que el caché de nivel inferior aplica al caché de página deben establecer relaciones de mapeo uno por uno en unordered_map (NewSpan)

2. El caché de la página recicla el intervalo almacenado en caché centralmente y fusiona intervalos adyacentes para crear una asignación (ReleaseSpanToPageCache)

        Aunque la eficiencia de búsqueda de unordered_map es O (1), para evitar el impacto de la inserción en un entorno de subprocesos múltiples, los bloqueos deben bloquearse durante la búsqueda. Para garantizar la seguridad del subproceso, la relación de mapeo no se debe agregar durante la búsqueda, porque la expansión del hash o el nuevo nodo de la lista vinculada en caso de conflicto afectará la precisión de la búsqueda. El árbol de base utiliza una relación de mapeo uno a uno. La ID convertida desde 4G de memoria tendrá un pozo exclusivo para el almacenamiento. Al mismo tiempo, solo un hilo puede leer o escribir en una determinada ubicación al mismo tiempo. De esta manera, las búsquedas adicionales de todos los objetos de la memoria no se afectan entre sí y no es necesario volver a bloquear durante la búsqueda.

Supongo que te gusta

Origin blog.csdn.net/gfdxx/article/details/131116337
Recomendado
Clasificación