首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >STL容器性能探秘:stack、queue、deque的实现与CPU缓存命中率优化

STL容器性能探秘:stack、queue、deque的实现与CPU缓存命中率优化

作者头像
云泽808
发布2025-12-30 18:35:28
发布2025-12-30 18:35:28
1110
举报

前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、stack的模拟实现

对于一般人来说模拟实现的栈的底层就是一个数组,让数组尾部做栈顶。无论是数组栈还是链式栈,很多的东西都是和顺序表和链表是类似的

但是STL中的栈不是这样实现的,STL在设计初的考量就是相似的一些东西直接复用,像实现一个数组栈,直接如图封装一个vector实现,就不用再实现顺序表和链表的内部的一些逻辑了。

在这里插入图片描述
在这里插入图片描述

1.1 适配器Container的封装概念

且C++设计STL的这些大佬的思路还更开阔一些,他们还加入了一种叫做容器适配器(Container,转换的意思)的东西

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这样封装适配器的思路是非常牛的 “自己从头实现栈” 的写法(比如注释里的T* _a+_top+_capacity)—— 需要自己管理动态数组的扩容、释放;而现在这个stack是 “复用现有容器(比如vector/list)的功能,包装成栈的接口”,底层存储完全交给Container(比如vector),自己只封装 “栈需要的操作”(push/pop/top 等)。

这种适配结构的核心优点

  1. 【少写重复代码】复用现有容器,不用自己实现底层存储 原来注释里的 “数组栈” 需要自己写:
    • 动态数组的初始化、扩容(比如_capacity满了要realloc);
    • 内存的释放(防止内存泄漏);
    • 下标访问、边界检查等。 而现在的stack直接用vector/list作为底层容器 —— 这些容器本身已经实现了动态扩容、内存管理、尾插 / 尾删等功能,你只需要调用_con.push_back()/_con.pop_back(),相当于 “站在现有容器的肩膀上”,不用重复造轮子。
  1. 【灵活切换场景】换底层容器只改模板参数,不用动栈的逻辑 代码里用的是vector< int >作为底层(yunze::stack<int, vector< int >>),但如果场景变了:
  • 比如需要 “频繁在栈顶插入 / 删除,且不想承担vector扩容的开销”—— 只需要把模板参数换成list< int >,代码改成:
代码语言:javascript
复制
yunze::stack<int, list<int>> st; // 底层变成链表,其他代码完全不用改
st.push(1); // 还是用同样的push接口
st.top();   // 还是用同样的top接口

底层容器换了,但栈的调用代码完全不用改—— 因为stack封装了统一的接口,不管底层是vector还是list,对外都是 “栈的操作”。

  1. 【统一接口】不用关心底层实现,只用记 “栈的用法” 不管底层是vector(数组)还是list(链表),你用这个stack时,只需要调用push/pop/top—— 不用关心底层是 “数组尾插” 还是 “链表尾插”,也不用记vector的push_back或list的push_back细节,只需要记 “栈的标准操作” 即可,降低了学习和使用的成本。
  2. 【稳定可靠】借现有容器的成熟性,减少 bug STL 里的vector/list是经过大量测试、优化的容器,比自己写的 “数组栈”(比如注释里的T* _a)更稳定:
  • 比如vector的扩容逻辑是经过性能优化的(通常是 2 倍扩容,减少频繁申请内存的开销);
  • list的尾插 / 尾删是 O (1) 且不会有内存碎片问题;
  • 这些容器本身也做了边界检查(比如empty()时调用back()会报错)。 用它们作为底层,比自己手写的存储结构更不容易出 bug。
  1. 【适配多类型】底层容器支持的类型,栈都能直接用 比如你想存string类型的栈,只需要写:
代码语言:javascript
复制
yunze::stack<string, vector<string>> st;
st.push("hello");
cout << st.top() << endl; // 输出hello

因为vector< string >本身支持string类型,你的stack不需要额外写任何代码,直接适配所有Container支持的类型。

看到这里可能看着该全新的写法还有一些疑问,下面再看一下具体的模板参数实例化和函数调用过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

库中的STL还支持使用缺省参数deque(一个容器),deque既不是一个数组栈,也不是一个链式栈,而是一个双端队列适配出来的栈,双端队列不要求先进先出,其在功能上是list和vector的融合体

在这里插入图片描述
在这里插入图片描述

二、queue的模拟实现

队列是队尾入数据,队头出数据

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

也可以在queue的类中用erase代替pop_front();vector不支持直接头删,但是erase是支持头删的,但是这就让效率强行降低了,STL中的vector设计之初没有直接支持头删接口的原因就是希望少用(功能上还是支持的)

代码语言:javascript
复制
void pop()
{
	_con.erase();
}

三、deque的介绍

deque(双端队列):是一种双开口的“连续”空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),也可以在中间任意位置插入删除,与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高,其在功能上是list和vector的融合体,

在这里插入图片描述
在这里插入图片描述

接口和list和vector的接口都基本是类似的

3.1 deque的使用

在这里插入图片描述
在这里插入图片描述

仅管从功能上看deque好像可以替代vector和list,但是在实际情况下是不可以的,如果可以的话STL就没有vector和list两个容器了

deque更像是一个有着很好的设计初衷,为了解决vector和list的问题,但实际成品没有达到预期的一个容器,但是其也有很多可取之处 下图是vector和list的优缺点

在这里插入图片描述
在这里插入图片描述

3.2 CPU高速缓存访问命中率与数据访问效率

3.2.1 数据访问效率

CPU高速缓存访问命中率高间接的也就是说数据访问效率高,反之。该优缺点在归并排序和快速排序的性能测试尤其体现,这里的性能测试该篇文章有写C++ List 容器详解:迭代器失效、排序与高效操作

在这里插入图片描述
在这里插入图片描述

这里上面打错了,是100w个数据的数组。 排序的性能差异就是由于数据的访问效率低,排序的过程就要对数据结构中的数据进行大量的访问和交换,数据越多,这个差异就体现出来了

3.2.2 CPU高速缓存访问命中率

下面说一下CPU高速缓存访问命中率这个概念 一、计算机存储介质的层级特性 计算机的存储体系核心是 “速度越快→容量越小→成本越高” 的层级设计,从慢到快、从大到小分为:

  1. 硬盘:永久存储介质(断电数据不丢失),容量极大(几百 GB~ 数 TB),但访问速度最慢(毫秒级)。程序 / 文件的持久化存储依赖硬盘,读取硬盘数据时,需先将数据加载到内存中才能被 CPU 访问;
  2. 内存:临时存储介质(断电数据丢失),容量适中(8GB/16GB/32GB 为主),访问速度远快于硬盘(几十纳秒级),但仍慢于 CPU 运算速度。所有运行中的程序、数据结构(如 vector/list)的实际数据都存储在内存中;
  3. CPU 缓存(L1/L2/L3):介于内存和寄存器之间的高速存储,容量远小于内存(L1 几十 KB、L2 几百 KB、L3 几 MB~ 几十 MB),访问速度是内存的 10~100 倍(纳秒级);
  4. 寄存器:CPU 内部的超高速存储(几字节~几十字节),速度最快(亚纳秒级),但容量极小,仅能存储 CPU 运算时的临时数据(如 ++ 操作的中间值)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

CPU 执行指令的核心逻辑:为什么缓存是刚需 以代码for(auto& e : con) { e++; }为例,CPU 执行 “e++” 的本质是:

  1. 从存储介质中读取 e 的数值;
  2. 把数值加载到寄存器中,通过加法器完成 ++ 运算;
  3. 把运算后的数值写回存储介质。

如果直接从内存读取 / 写入e,会因为内存速度远慢于 CPU 运算速度,导致 CPU 大部分时间处于 “等待数据” 的空闲状态(即 “CPU stall”)。因此 CPU 设计了缓存层级

  • 优先从缓存 / 寄存器读取数据,只有缓存中没有目标数据时,才从内存加载;
  • 加载数据时,不是 “要多少读多少”,而是基于 “局部性原理” 批量加载,最大化缓存的利用率。
在这里插入图片描述
在这里插入图片描述

CPU 三级缓存的结构与访问规则 如四核 CPU 缓存架构图所示:

  1. 缓存的层级归属
    • L1 缓存:每个 CPU 核心独占,且分为 “数据缓存(Data Cache)” 和 “指令缓存(Instruction Cache)”—— 程序的执行指令(如 for 循环的跳转、++ 运算指令)会加载到指令缓存,数据(如 vector/list 的元素)会加载到数据缓存;
    • L2 缓存:每个 CPU 核心独占,不区分数据 / 指令,容量比 L1 大,速度略慢;
    • L3 缓存:所有 CPU 核心共享,容量最大,速度比 L2 慢,但仍远快于内存。
  1. 缓存行(Cache Line):数据加载的最小单位: CPU 从内存向缓存加载数据时**,必须按 “缓存行” 加载**(主流 CPU 的缓存行大小为 64 字节)—— 哪怕只需要 1 个 4 字节的int变量,CPU 也会把该变量所在的 “64 字节连续内存块” 全部加载到缓存中。 原因:加载数据的硬件成本(如内存总线的 “探测线” 读取 0/1 比特位)与加载量无关,类似 “开公交车接人,接 1 人和接 30 人(64 字节)的发车成本一致”,不如一次性加载更多数据。
  2. 缓存命中 / 未命中的访问流程: CPU 访问数据时,按 “L1→L2→L3→内存” 的顺序检查:
    • 命中(Hit):目标数据在某一级缓存中,直接读取,效率最高;
    • 未命中(Miss):目标数据不在缓存中,需先从内存加载 “目标数据所在的 64 字节缓存行” 到 L3→L2→L1 缓存,再从缓存读取(此过程会产生 “缓存缺失延迟”,是性能瓶颈的核心)。

局部性原理:缓存设计的核心依据 CPU 缓存的批量加载逻辑,完全基于局部性原理(计算机程序的核心运行规律):

  1. 时间局部性:访问过的某个内存地址数据,短期内大概率会再次访问(如 for 循环的迭代变量i会被多次读取 / 修改);
  2. 空间局部性:访问过的某个内存地址数据,其相邻的连续内存地址数据短期内大概率会访问(如数组的第 i 个元素后,大概率会访问第 i+1 个元素)。

基于这一原理,CPU 加载 “当前数据 + 后续连续数据” 到缓存,能最大化 “缓存命中” 的概率 —— 这也是vector和list缓存命中率差异的核心根源。

五、缓存命中率的定义与核心影响 缓存命中率 = 缓存命中的访问次数 / 总数据访问次数 × 100%。

  • 命中率越高:CPU 越少访问慢内存,数据访问效率越高,程序执行速度越快;
  • 命中率越低:CPU 频繁触发 “缓存未命中→内存加载”,大量时间浪费在等待数据上,效率越低。

缓存污染 缓存容量是有限的,若加载了大量 “后续不会被访问” 的数据到缓存,会挤占 “有用数据” 的缓存空间,导致有用数据被挤出缓存→后续访问有用数据时触发 “未命中”,这种现象称为缓存污染

  • 例如:遍历一个分散存储的 list 时,每次加载的缓存行只包含 1 个有效节点,其余 60 + 字节都是无用数据,这些无用数据会挤占缓存空间,导致其他有用数据被污染,进一步降低命中率。
在这里插入图片描述
在这里插入图片描述
  1. vector:极致利用缓存→命中率接近 100%vector<int> v = {1,2,3,4,5,...,20}为例(int 占 4 字节,20 个元素占 80 字节):
  • 当 CPU 访问第一个元素v[0]=1(地址0x100)时,触发 “缓存未命中”,CPU 从内存加载0x100~0x13F(64 字节,包含前 16 个元素:1~16)到 L1 缓存;
  • 访问v[1]=2~v[15]=16时,这些元素已在 L1 缓存中,100% 命中,直接读取;
  • 访问v[16]=17时,触发第二次 “未命中”,加载0x140~0x17F(64 字节,包含 17~20)到缓存,后续访问17~20仍为命中。

整个遍历过程中,仅触发 2 次缓存未命中,其余 18 次均为命中,命中率≈90%;且 vector 无缓存污染(加载的缓存行全是有效元素)。

  1. list:完全违背缓存设计→命中率接近 0list<int> l = {1,2,3,4}为例:
  • 访问第一个节点1(地址0x100):未命中,加载0x100~0x13F到缓存(但该缓存行中只有0x100是有效节点 1,其余 60 字节均为无用数据,缓存污染);
  • 访问第二个节点2(地址0x200):该地址不在上一个缓存行中,触发未命中,加载0x200~0x23F到缓存(仅0x200是有效节点 2);
  • 访问节点 3、4 时,重复上述过程,每访问一个节点都触发未命中,命中率≈0%。

且每次加载的缓存行中,有效数据仅占 4/64=6.25%,其余均为无用数据,严重污染缓存→挤占其他有用数据的缓存空间,进一步降低整体效率。

在这里插入图片描述
在这里插入图片描述

还有专门写该部分原理的博客程序员相关的CPU缓存知识

四、完整源码

queue.h

代码语言:javascript
复制
#pragma once

#pragma once
#include<iostream>
#include<vector>
#include<list>
#include<deque>
using namespace std;

namespace yunze
{
	template<class T, class Container = deque<T>>
	class queue
	{
	public:
		//队尾入数据
		void push(const T& x)
		{
			//Container若不支持push_back就会报错,说明容器不适配
			_con.push_back(x);
		}

		//队头出数据
		void pop()
		{
			_con.pop_front();
		}
		
		//取队头数据
		const T& front()
		{
			
			return _con.front();
		}

		//取队尾数据
		const T& back()
		{

			return _con.back();
		}

		size_t size() const
		{
			return _con.size();
		}

		bool empty() const
		{
			return _con.empty();
		}

	private:
		//vector<T> _v;
		Container _con;
	};
}

stack.h

代码语言:javascript
复制
#pragma once
#include<iostream>
#include<vector>
#include<list>
#include<deque>
using namespace std;

namespace yunze
{
	//template<class T>
	//class stack //数组栈
	//{
	//	// ...
	//private:
	//	T* _a;
	//	size_t _top;
	//	size_t _capacity;
	//};
	template<class T, class Container = deque<T>>
	class stack
	{
	public:
		//构造析构拷贝不用写,没有其他需求,默认生成的够用
		void push(const T& x)
		{
			//Container若不支持push_back就会报错,说明容器不适配
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_back();
		}

		//top这里不用[],因为不一定是vector
		const T& top()
		{
			//容器一般都有一个front和back接口,如vector和list
			return _con.back();
		}

		size_t size() const
		{
			return _con.size();
		}

		bool empty() const
		{
			return _con.empty();
		}

	private:
		//vector<T> _v;
		Container _con;
	};
}

test.cpp

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 666
#include"stack.h"

//int main()
//{
//	//yunze::stack<int, vector<int>> st;//数组栈
//	//yunze::stack<int, list<int>> st;//数组栈
//	yunze::stack<int> st;
//	st.push(1);
//	st.push(2);
//	st.push(3);
//	st.push(4);
//
//	while (!st.empty())
//	{
//		cout << st.top() << " ";
//		st.pop();
//	}
//	cout << endl;
//	return 0;
//}

#include"queue.h"

////默认也是用deque适配
//int main()
//{
//	yunze::queue<int> q;
//	//不能使用vector适配,vector不支持头删
//	//yunze::queue<int, vector<int>> q;
//	
//	//队列只能使用list适配
//	//yunze::queue<int, list<int>> q;
//	q.push(1);
//	q.push(2);
//	q.push(3);
//	q.push(4);
//	while (!q.empty())
//	{
//		cout << q.front() << " ";
//		q.pop();
//	}
//	cout << endl;
//	return 0;
//}

#include<deque>

int main()
{
	deque<int> dp;
	dp.push_back(1);
	dp.push_back(1);
	dp.push_back(1);
	dp.push_front(2);
	dp.push_front(3);
	dp.push_front(4);
	dp[0] += 10;
	for (auto e : dp)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、stack的模拟实现
    • 1.1 适配器Container的封装概念
  • 二、queue的模拟实现
  • 三、deque的介绍
    • 3.1 deque的使用
    • 3.2 CPU高速缓存访问命中率与数据访问效率
      • 3.2.1 数据访问效率
      • 3.2.2 CPU高速缓存访问命中率
  • 四、完整源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档