九、idSpanMap使用基数树代替原本的unordered_map 十、使用基数树前后性能对比

news2025/1/16 6:50:42

九、idSpanMap使用基数树代替原本的unordered_map

我们原本的idSpanMap用的是STL容器中的unordered_map哈希桶,因为STL的容器本身是不保证线程安全的,所以我们在访问时需要加锁保证线程安全,这也就是我们写的内存池的性能的瓶颈点。因为我做的这个内存池的项目是参照谷歌的开源项目tc-malloc,然后自主实现的mini版本,我查看tc-malloc的源码的优化策略是利用一颗基数树来代替stl中的unordered_map的,因为我们的需求是要保存页号和Span的映射关系,Span是指针本质也是一个整数,也就是说我们要保存的是<int,int>的映射关系,而基数树正好可以满足我们的需求,所以基数树就成为了优化我们的内存池的不二人选,并且用这个基数树定义idSpanMap最大的好处是访问idSpanMap时不需要加锁也能保证线程安全的。那它是怎么做到的呢?接下来我们就来学习一下。

9.1 什么是基数树?

基数树说白了也是一种哈希结构,基数树分为一层基数树,两层基数树,三层基数树。

9.1.1 一层基数树

一层基数树采用的是直接映射的方法

32位机器下才可以用一层基数树,64位机器下不可以,因为2^62次方太大了,早已超出了我们机器的内存的大小了,所以64位机器要用两层基数树或者三层基数树。
在这里插入图片描述

9.1.2 两层基数树

两层基数树是把哈希映射的关系分成两层,第一层是利用低19位的高5位判断该id在第一层的哪一个位置,再按照低14位判断在第二层的哪个位置。

在这里插入图片描述
三层基数树也是按照两层基数树的模式继续延伸下去即可,在这也就不过多叙述了,详情可以看后面的代码实现。

9.2 tcmalloc中基数树的源码实现

//基数树,用来代替 idSpanMap,访问时可以不用加锁

// Single-level array
template <size_t BITS>
class TCMalloc_PageMap1 {
private:
	static const int LENGTH = 1 << BITS;
	void** array_;

public:
	typedef uintptr_t Number;

	//explicit TCMalloc_PageMap1(void* (*allocator)(size_t)) {
	explicit TCMalloc_PageMap1() {
		//array_ = reinterpret_cast<void**>((*allocator)(sizeof(void*) << BITS));
		size_t size = sizeof(void*) << BITS;
		size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT);
		array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT);
		memset(array_, 0, sizeof(void*) << BITS);
	}

	// Return the current value for KEY.  Returns NULL if not yet set,
	// or if k is out of range.
	void* get(Number k) const {
		if ((k >> BITS) > 0) {
			return NULL;
		}
		return array_[k];
	}

	// REQUIRES "k" is in range "[0,2^BITS-1]".
	// REQUIRES "k" has been ensured before.
	//
	// Sets the value 'v' for key 'k'.
	void set(Number k, void* v) {
		array_[k] = v;
	}
};

// Two-level radix tree
template <int BITS>
class TCMalloc_PageMap2 {
private:
	// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
	static const int ROOT_BITS = 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;

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Leaf* root_[ROOT_LENGTH];             // Pointers to 32 child nodes
	void* (*allocator_)(size_t);          // Memory allocator

public:
	typedef uintptr_t Number;

	//explicit TCMalloc_PageMap2(void* (*allocator)(size_t)) {
	explicit TCMalloc_PageMap2() {
		//allocator_ = allocator;
		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];
	}

	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;
	}

	bool Ensure(Number start, size_t n) {
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> LEAF_BITS;

			// Check for overflow
			if (i1 >= ROOT_LENGTH)
				return false;

			// Make 2nd level node if necessary
			if (root_[i1] == NULL) {
				//Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				//if (leaf == NULL) return false;
				static ObjectPool<Leaf>	leafPool;
				Leaf* leaf = (Leaf*)leafPool.New();

				memset(leaf, 0, sizeof(*leaf));
				root_[i1] = leaf;
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
		// Allocate enough to keep track of all possible pages
		Ensure(0, 1 << BITS);
	}
};

// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
	// How many bits should we consume at each interior level
	static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
	static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;

	// How many bits should we consume at leaf level
	static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;

	// Interior node
	struct Node {
		Node* ptrs[INTERIOR_LENGTH];
	};

	// Leaf node
	struct Leaf {
		void* values[LEAF_LENGTH];
	};

	Node* root_;                          // Root of radix tree
	void* (*allocator_)(size_t);          // Memory allocator

	Node* NewNode() {
		Node* result = reinterpret_cast<Node*>((*allocator_)(sizeof(Node)));
		if (result != NULL) {
			memset(result, 0, sizeof(*result));
		}
		return result;
	}

public:
	typedef uintptr_t Number;

	explicit TCMalloc_PageMap3(void* (*allocator)(size_t)) {
		allocator_ = allocator;
		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];
	}

	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);
		reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v;
	}

	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);

			// Check for overflow
			if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
				return false;

			// Make 2nd level node if necessary
			if (root_->ptrs[i1] == NULL) {
				Node* n = NewNode();
				if (n == NULL) return false;
				root_->ptrs[i1] = n;
			}

			// Make leaf node if necessary
			if (root_->ptrs[i1]->ptrs[i2] == NULL) {
				Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
				if (leaf == NULL) return false;
				memset(leaf, 0, sizeof(*leaf));
				root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
			}

			// Advance key past whatever is covered by this leaf node
			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}

	void PreallocateMoreMemory() {
	}
};

由于上面的基数树是更改idSpanMap的结构的,所以需要把idSpanMap的访问操作修改成对应的基数树的set和get
idSpanMap主要是在PageCache中使用,修改后如下:

PageCache PageCache::_sInst;

//k代表的是这个span的大小k页
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	//如果申请的span的大小大于128页,则需要直接向堆申请
	if (k > NPAGES - 1)
	{
		//向堆申请k页内存
		void* ptr = SystemAlloc(k);
		Span* kSpan = _spanPool.New();
		//地址转化成页号
		kSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		kSpan->_n = k;
		//把页号和Kspan的映射关系放进Map中
		//_idSpanMap[kSpan->_pageId] = kSpan;
		_idSpanMap.set(kSpan->_pageId, kSpan);
		return kSpan;
	}
	else
	{
		//如果PageCache第k个位置的哈希桶上有k页大小的span,则直接返回一个span
		if (!_spanLists[k].Empty())
		{
			Span* kSpan = _spanLists[k].PopFront();

			//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
			//CentralCache回收小块内存时,查找对应的span
			//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
			//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				//_idSpanMap[kSpan->_pageId + i] = kSpan;
				_idSpanMap.set(kSpan->_pageId + i, kSpan);
			}

			return kSpan;
		}

		//走到这里说明PageCache第k个位置的哈希桶没有k页大小的span,则需要遍历
		//后面的大于k页的哈希桶,找到了一个n页大小的span就把这个span切分成一个
		// k页大小的span和一个n-k页大小的span,k页的返回,n-k页的挂到对应的哈希桶中

		//遍历后面的哈希桶
		for (size_t i = k + 1; i < NPAGES; i++)
		{
			//找到了一个不为空的i页的哈希桶,就对它进行切分
			if (!_spanLists[i].Empty())
			{
				//k页的span
				Span* kSpan = _spanPool.New();
				//n页的span
				Span* nSpan = _spanLists[i].PopFront();

				//开始把一个n页的span切分成一个k页的span和一个n-k页的span
				// 
				//从nSpan的头上切k页给kSpan,所以kSpan的页号就是nSpan的页号
				kSpan->_pageId = nSpan->_pageId;
				kSpan->_n = k;//kSpan的页数是k

				//被切分以后nSpan的页号需要+=k页,因为头nSpan的头k页已经切分给了kSpan
				nSpan->_pageId += k;
				nSpan->_n -= k;//nSpan的页数要-=k页,因为nSpan被切走了k页

				//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
				// CentralCache回收小块内存时,查找对应的span
				//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
				//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
				for (PAGE_ID i = 0; i < kSpan->_n; i++)
				{
					//_idSpanMap[kSpan->_pageId + i] = kSpan;
					_idSpanMap.set(kSpan->_pageId + i, kSpan);
				}

				//nSpan被切分后的首页和尾页的页号和nspan的映射关系也需要保存起来
				//以便后续合并,因为合并的方式是前后页合并,往前找肯定找到的是一个span的
				//最后一页,往后找一定找的是一个span的第一页,所以挂在PageCache对应哈希桶
				//的span的第一页和最后一页与span的关系也需要保存起来
				// 
				//_idSpanMap[nSpan->_pageId] = nSpan;
				//_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
				_idSpanMap.set(nSpan->_pageId, nSpan);
				_idSpanMap.set(nSpan->_pageId + nSpan->_n - 1, nSpan);

				if (nSpan->_pageId == 0)
				{
					int x = 0;
				}

				//把剩余的n-k页的span头插到对应下标的哈希桶中
				_spanLists[nSpan->_n].PushFront(nSpan);
				return kSpan;
			}
		}

		//走到这里说明前面的NPAGES个哈希桶中都没有Span,(例如第一次申请内存时)
		//则需要向堆申请一个128页大小的span大块内存,挂到对应的哈希桶中
		void* ptr = SystemAlloc(NPAGES - 1);

		Span* bigSpan = _spanPool.New();
		bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//内存的地址需要转换成页号映射到对应的哈希桶中
		bigSpan->_n = NPAGES - 1;

		//把NPAGES-1页大小的span头插到对应NPAGES-1号桶中去
		_spanLists[bigSpan->_n].PushFront(bigSpan);

		//本质是运用了复用的设计,避免代码中出现重复的逻辑
		return NewSpan(k);
	}

}

Span* PageCache::MapObjectToSpan(void* obj)
{
	//计算出obj对应的页号
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;

	访问_idSpanMap的时候需要加锁,避免线程安全的问题
	这里使用C++11的RAII锁,出了这个函数这把锁会自动解掉
	//std::unique_lock<std::mutex> lock(_pageMtx);

	通过页号查找该内存块对应的是哪一个span
	//auto ret = _idSpanMap.find(id);
	//if (ret != _idSpanMap.end())
	//{
	//	return ret->second;
	//}
	//else
	//{
	//	assert(false);
	//	return nullptr;
	//}

	//换成了基数树作为map存放页号和Span*的映射关系之后,访问的时候是不需要再加锁的,原因有以下几点:
	//1、只有在NewSpan函数和ReleaseSpanToPageCache函数会去写基数树。
	//2、基数树在写之前就会开好空间,写的过程中是不会影响到基数树的结构的,也就是说在两个线程在访问
	// 不同位置时互相是不会受到影响的。
	//3、基数树对同一个位置的读写是分离的,线程1对一个位置进行读写的时候,线程2不可能也在对同一个位置进行读写
	//写是在申请span和释放span的时候,是在没人用的时候做的;而读是在有人用这个span的时候做的,所以读写是分离的
	// 4、另一方面就是NewSpan函数和ReleaseSpanToPageCache函数在调用之前本身就已经加锁了,所以这里就不用加锁了
	//但是为什么用stl下的map要加锁呢?本质是红黑树在插入的时候会改变树的结构,一个线程在插入节点改变红黑树的结构,
	//一个线程在遍历就会有线程安全的问题,而基数树插入的时候不会影响结构,而且基数树读取的时候并不是遍历,而是直接通过下标
	//就访问到了对应的位置的
	
	//通过页号查找该内存块对应的是哪一个span
	void* ret = _idSpanMap.get(id);
	if (ret != nullptr)
	{
		return (Span*)ret;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

//CentralCache把span还回来给PageCache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//如果span的页数大于128页,则说明这个span是从堆上直接申请的,
	//直接释放给堆即可,不能挂到PageCache的哈希桶中,因为PageCache一个只有128个桶
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		_spanPool.Delete(span);
		return;
	}

	while (1)
	{
		//找前一页的span,看是否能够和当前页合并,如果能,则循环向前合并,直到不能合并为止
		PAGE_ID prevId = span->_pageId - 1;
		void* ret = _idSpanMap.get(prevId);

		//_idSpanMap中没找到前一页和对应span,说明前一页的内存没有被申请,结束合并
		if (ret == nullptr)
		{
			break;
		}

		Span* prevSpan = (Span*)ret;
		//如果前一页对应的span在CentralCache中正在被使用,结束合并
		if (prevSpan->_isUse == true)
		{
			break;
		}

		//如果和前一页合并之后会超过哈希桶的最大的映射返回,结束合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		//合并span和prevSpan
		span->_pageId = prevSpan->_pageId;
		span->_n = prevSpan->_n + span->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[prevSpan->_n].Erase(prevSpan);
		//因为prevSpan已经被合并到了span中,所以prevSpan对应的内存可以delete掉了
		_spanPool.Delete(prevSpan);

	}

	while (1)
	{
		//找span的下一个span的起始页号
		PAGE_ID nextId = span->_pageId + span->_n;
		void* ret = _idSpanMap.get(nextId);
		if (ret == nullptr)
		{
			break;
		}

		Span* nextSpan = (Span*)ret;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES - 1)//曾经写成NPAGES+1了
		{
			break;
		}

		//span的起始页号不变,页数相加
		span->_n = span->_n + nextSpan->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[nextSpan->_n].Erase(nextSpan);
		_spanPool.Delete(nextSpan);
	}

	//合并得到的新的span需要挂到对应页数的哈希桶中
	_spanLists[span->_n].PushFront(span);
	//在PageCache中的span要设置为false,好让后面相邻的span来合并
	span->_isUse = false;
	//为了方便后续的合并,需要把span的起始页号和尾页号和span建立映射关系
	//_idSpanMap[span->_pageId] = span;
	//_idSpanMap[span->_pageId + span->_n - 1] = span;
	_idSpanMap.set(span->_pageId, span);
	_idSpanMap.set(span->_pageId + span->_n - 1, span);

}

代码中用的是一层基数树:
在这里插入图片描述

9.3 为什么使用基数树在访问时不需要加锁?

在这里插入图片描述

十、使用基数树前后性能对比

使用基数树优化前:我们的内存池比malloc还要稍微慢一些。
在这里插入图片描述

使用基数树优化后:我们的内存池的效率大概是malloc的10倍左右的样子,可见,基数树优化后的效率还是提高了不少的。
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/960658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】基础知识(二)

MySQL基础知识(二) 文章目录 MySQL基础知识(二)01 表操作1.1 创建表1.2 查看所有表1.3 查看指定表的结构1.4 删除表练习 02 CURD2.1 新增2.1.1 指定列插入2.1.2 datetime类型插入 2.2 查询2.2.1 全列查询2.2.2 指定列查询2.2.3 查询字段为表达式2.2.4 别名查询2.2.5 去重2.2.6 …

【Cookie和Session的那些事儿】

&#x1f320;作者&#xff1a;TheMythWS. &#x1f386;专栏&#xff1a;《集合与数据结构》 &#x1f387;座右铭&#xff1a;不走心的努力都是在敷衍自己&#xff0c;让自己所做的选择&#xff0c;熠熠发光。 目录 认识Cookie和Session Cookie Cookie对象的特点 Cookie对…

【算法日志】动态规划刷题:股票买卖附加问题(day42)

代码随想录刷题60Day 目录 前言 含冷冻期的股票买卖最佳时期 含手续费的股票买卖最佳时期 前言 今天的股票买卖问题会附加一些条件&#xff0c;但总体难度不大。 含冷冻期的股票买卖最佳时期 该问题难点在于对几个状态进行解构并写出相应的状态转移方程。 int maxProfit(ve…

每日一博 - 漫谈流控小妙招

文章目录 Pre概述实现方式固定窗口算法&#xff08;计数器法&#xff09;定义实现 滑动窗口算法定义实现 漏桶算法定义实现 令牌桶算法定义实现每秒产生5个令牌应对突发流量平滑预热 网关限流流量整形中间件限流 Pre 深入理解分布式技术 - 限流 并发编程-25 高并发处理手段之…

Revit SDK:AutoParameter 添加参数

前言 这个例子介绍如果往族文件里添加参数。 内容 Revit 的参数&#xff0c;参考官方文档&#xff1a; 这个例子的关键接口&#xff1a; // 通过 FamilyManager 添加参数 FamilyParameter AddParameter(string parameterName, BuiltInParameterGroup parameterGroup, Categ…

【JS案例】JS实现积分抽奖(内附源码)

JS案例实现积分抽奖 &#x1f31f;效果展示 &#x1f31f;HTML结构 &#x1f31f;CSS样式 &#x1f31f;实现思路 &#x1f31f;具体实现 1.定义抽奖次数渲染 2.点击抽奖按钮,实现滚动抽奖效果 3.弹窗处理 &#x1f31f;完整代码 &#x1f31f;写在最后 &#x1f3…

阿里面试经验分享:从被回绝到Offer,详解应聘阿里技术岗位注意事项

本文是《谈谈应聘阿里全流程》的姊妹篇&#xff0c;《谈谈应聘阿里全流程》发布后&#xff0c;收到了很多读者的积极反馈&#xff0c;但其中也反映出读者普遍的困惑&#xff1a;清楚了应聘流程&#xff0c;该如何有针对性地做应聘准备呢&#xff1f;这个问题从正面并不好回答&a…

windows自带远程桌面连接的正确使用姿势

摘要 目前远程办公场景日趋广泛&#xff0c;对远程控制的需求也更加多样化&#xff0c;windows系统自带了远程桌面控制&#xff0c;在局域网内可以实现流程的远程桌面访问及控制。互联网使用远程桌面则通常需要使用arp等内网穿透软件&#xff0c;市场上teamviewer、Todesk、向…

浅探Android 逆向前景趋势~

前段时间&#xff0c;我和朋友偶然间谈起安卓逆向&#xff0c;他问我安卓逆向具体是什么&#xff0c;能给我们带来什么实质性的东西&#xff0c;我也和朋友大概的说了一下&#xff0c;今天在这里拿出来和大家讨论讨论&#xff0c;也希望帮助大家来了解安卓逆向。 谈起安卓逆向…

C++面试题(叁)---操作系统篇

目录 操作系统篇 1 Linux中查看进程运行状态的指令、查看内存使用情况的指令、 tar解压文件的参数。 2 文件权限怎么修改 3 说说常用的Linux命令 4 说说如何以root权限运行某个程序。 5 说说软链接和硬链接的区别。 6 说说静态库和动态库怎么制作及如何使用&#xff0c;区…

sql:SQL优化知识点记录(六)

&#xff08;1&#xff09;索引优化1 查看一下有没有建立索引&#xff1a; 用到索引中的一个&#xff1a;type中的ref决定访问性能 用到索引中的两个&#xff1a;通过key_len的长度可以看出来&#xff0c;比第一个大一点。或者通过ref&#xff1a;中用到了两个常量const 用到了…

ubuntu系统安装QQ音乐

前言 要问程序员除了编程软件最离不开的最重要的是什么软件&#xff0c;个人觉得就是音乐软件了&#xff0c;所以QQ音乐单独拿出来整理一下&#xff0c;其他的软件放在一起整理一下。“一起长大的约定&#xff0c;那样真心&#xff0c;与你聊不完的曾经&#xff0c;而我已经分…

图像扭曲之万花筒

源码&#xff1a; void kaleidoscope(cv::Mat& src,cv::Mat& dst,double angle,double radius) {dst.create(src.rows, src.cols, CV_8UC3);dst.setTo(0);int cx src.cols / 2;int cy src.rows / 2;//angle PI / 4;double angle2 PI / 4;double sides radius / 3…

【板栗糖GIS】——360浏览器的下载图标隐藏在内部不方便,怎么修改

目录 1. 设置前的本来样子 2. 登录360的皮肤中心 3. 使用se13的经典皮肤 最近edge浏览器最近使用bilibili和notion都非常卡&#xff0c;时不时崩溃&#xff0c;不得不换浏览器使用&#xff0c;试来试去360浏览器最得我心&#xff0c;只不过广告太多&#xff0c;调教也是花了…

复杂性分析与算法设计:解锁计算机科学的奥秘

文章目录 算法复杂性分析的基本概念时间复杂度空间复杂度 常见的算法设计策略1. 分治法2. 贪心法3. 动态规划 算法设计的实际应用1. 网络路由2. 图像处理3. 人工智能 算法的选择和性能分析结论 &#x1f389;欢迎来到数据结构学习专栏~复杂性分析与算法设计&#xff1a;解锁计算…

excel 无法删除有合并单元格的列内容时的替代方法

背景&#xff1a; hp 笔记本电脑&#xff1b;win10 64位&#xff1b;excel 版本 16.0&#xff1b; office 2016自带excel 问题&#xff1a; 把pdf转excel后&#xff0c;由于原 pdf 图表本身的原因&#xff0c;转换后有不规则合并单元格的现象。 而在选择某列进行“删除” &a…

完整开发实现公众号主动消息推送,精彩内容即刻到达

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

【爬虫】实验项目三:验证码处理与识别

目录 一、实验目的 二、实验预习提示 三、实验内容 实验要求 基本要求&#xff1a; 改进要求A&#xff1a; 改进要求B&#xff1a; 四、实验过程 基本要求 五、源码如下 六、资料 一、实验目的 部分网站可能会使用验证机制来阻止用户无效登录或者是验证用户不是用程…

vue竖向步骤条

效果图&#xff1a; 弹框组件代码&#xff1a; <template><el-dialog:visible.sync"dialogVisible":append-to-body"true":close-on-click-modal"false":close-on-press-escape"false"titlewidth"8.2rem"custom-c…

Redis 缓存穿透、击穿、雪崩

一、缓存穿透 1、含义 缓存穿透是指查询一个缓存中和数据库中都不存在的数据&#xff0c;导致每次查询这条数据都会透过缓存&#xff0c;直接查库&#xff0c;最后返回空。 2、解决方案 1&#xff09;缓存空对象 就是当数据库中查不到数据的时候&#xff0c;我缓存一个空对象…