CPU相关基础知识及优化技巧

发布时间:2026/6/8 22:34:14

CPU相关基础知识及优化技巧 CPU 背景CPU 中文称为“中央处理器”主要是通过内部电路晶体管进行数学和逻辑运算执行一系列指令来完成任务。CPU底层组成晶体管门电路加法器ALU运算器用于对数据进行加工控制器用于协调并控制计算机各部件执行程序的指令序列基本功能包括取指令、分析指令、执行指令。运算器的核心就是ALU算数逻辑单元通过组合逻辑电路负责进行算术运算、逻辑运算和移位操作用来进行数值计算和产生存储器访问地址。两个操作数A,B经过ALU电路处理后可以得到一个输出结果。参与运算的数据需要提前放到某些通用寄存器中所以运算器的内部还需要提供通用寄存器组。累加寄存器暂存ALU运算的中间结果或操作数数据缓冲寄存器临时存放从内存读取的数据状态条件寄存器记录运算结果的状态如是否溢出、结果是否为零等控制器的基本结构主要有程序计数器用于指出下一条指令在主存中的存放地址。CPU就是根据PC的内容去主存中取指令的。因程序中指令(通常)是顺序执行的所以PC有自增功能。指令寄存器用于保存当前正在执行的那条指令。指令译码器:仅对操作码字段进行译码向控制器提供特定的操作信号。时序系统用于产生各种时序信号它们都是由统一时钟(CLOCK)分频得到。微操作信号发生器根据IR的内容(指令)、PSW的内容(状态信息)及时序信号产生控制整个计算机系统所需的各种控制信号其结构有组合逻辑型和存储逻辑型两种。寄存器用于寻址和计算过程的产生的地址和数据CPU就像一个工厂控制器是调度中心PC规划路线运算器是生产线ALU执行操作寄存器是临时仓库暂存原料和产品CPU之流水线设计 -- Pipeline经典的5级流水线设计指令取指是指将指令从存储器中读取出来的过程。指令译码是指将从存储器中取出的指令进行翻译的过程。经过译码之后得到指令需要的操作数 寄存器索引可以使用此索引从通用寄存器组中将操作数读出指令译码之后所需要进行的计算类型都己得知并且己经从通用寄存器组中读取出了所需的操作数那么接下来便进行指令执行。指令执行是指对指令进行真正运算的过程。 比如如果指令是一条加法运算指令则对操作数进行加法操作如果是减法运算指令则进行减法操作。在执行阶段最常见的算术逻辑部件就是ALU。访存是指存储器访问指令将数据从存储器中读出或者写入存储器的过程。写回是指将指令执行的结果写回通用寄存器组的过程 如果是普通运算指令该结果值来自于“执行”阶段计算的结果如果是存储器读指令该结果来自于“访存”阶段从存储器中读取出来的数据。流水线的核心逻辑并行处理把 “指令处理” 拆分成 5 个独立工位对应 5 个步骤每个工位只负责自己的步骤且上一个指令进入下一个工位后下一个指令立刻进入当前工位。从第 5 个周期开始每个周期都能完成 1 条指令。Pipeline stall 流水线阻塞具体指某个步骤卡住后续所有步骤都必须等待。比如说int a b c; // 指令1计算a结果需周期5写回 int d a 1; // 指令2需要a的值但指令1在周期5才写回指令2的EX步骤周期4会阻塞常见的有三种数据依赖Data Hazard后一条指令需要前一条的结果”具体解释为如果指令 B 需要用到指令 A 的计算结果但指令 A 的 “写回WB” 步骤还没完成指令 B 的 “执行EX” 步骤就会卡住等待数据。int a b c; // 指令1计算a结果需周期5写回 int d a 1; // 指令2需要a的值但指令1在周期5才写回指令2的EX步骤周期4会阻塞启发写代码时尽量减少 “紧密的数据依赖”比如通过编译器优化如 -O2让编译器自动调整指令顺序或手动拆分无关计算如将独立的数组初始化和计算分开。控制依赖分支跳转导致指令预取错误CPU 的 “取指IF” 步骤会提前预取后续指令基于 “程序大概率顺序执行” 的假设但如果遇到分支指令如if-else、for循环的条件判断一旦跳转方向与预取方向不符预取的指令就会 “作废”流水线需要清空并重新从跳转目标地址取指导致阻塞。if (x 0) { // 分支指令CPU预取“x0”的执行路径指令A a 1; // 指令A } else { a 2; // 指令B若x≤0预取的指令A作废需重新取指令B流水线阻塞3-4个周期 }启发AI/C 代码中避免 “频繁的小分支”如循环内的if判断可通过 “分支消除” 优化如用查表法、位运算替代分支或利用 CPU 的 “分支预测器”如让分支结果 “大概率为真”减少预测错误。结构依赖多个指令争抢同一硬件资源CPU 内部部分硬件资源是共享的如访存单元、ALU如果两条指令同时需要使用同一资源就会导致其中一条阻塞。指令 A 和指令 B 都需要 “访存MEM” 步骤但 CPU 只有一个访存单元指令 B 必须等待指令 A 的 MEM 步骤完成后才能执行指令执行平均周期数CPI总周期数 流水线级数 (指令数 - 1)平均 CPI 总周期数 / 指令数为什么指令不能等内存CPU 与内存的 “速度差距过大”—— 如果指令执行过程中频繁等待内存会导致 CPU 大量时间被闲置严重浪费性能存储器存储器分类RAM全称是 Random Access Memory随机存取存储器程序运行时临时使用的工作区断点后数据会丢失ROM全称是 Read Only Memory只读存储器保存固定程序断点数据不丢失。存储器层次结构一般我们常见的CPURAM以及Cache之间的关系大致如下CPU - Cache - RAM - ssd / hdd 外存ROM通常不在这个运行数据的访问链路中。SRAM静态随机存取存储器用于L1、L2、L3 CacheCPU 核心紧邻的高速缓存核心优势是极致低延迟适配 CPU 对 “即时数据” 的高速需求DRAM动态随机存取存储器用于L4 Cache部分高端 CPU / 服务器会配置又称 “近核 DRAM 缓存”核心优势是大容量 低成本作为 L3 Cache 与主内存普通 DRAM之间的 “过渡层”平衡速度与容量。分层的目的分层设计的本质是为了平衡 “速度、容量、成本” 三者的矛盾——CPU 对数据的访问存在 “高频小范围” 和 “中频大范围” 的差异单一容量的 SRAM 无法同时满足 “极致速度” 和 “足够容量” 的需求。三层 Cache 的最大区别是与 CPU 核心的物理距离—— 距离越近信号传输延迟越低、速度越快但容量越难做大。CPU 访问数据的效率核心看 “Cache 命中率”即要访问的数据是否在缓存中—— 命中率越高越不需要去主内存DRAM读数据整体速度越快。L1-L3 的分层设计通过 “分工” 大幅提升命中率具体逻辑如下L1专注 “极致速度”解决 “最高频访问”L1 的速度依赖 “与运算单元的物理距离”—— 距离越近信号传输越快。若 L1 容量从 32KB 扩大到 1MB物理面积会增加几十倍必须远离运算单元延迟会从 3ns 飙升到 10ns 以上越快越小L2作为 “中间桥梁”降低 L1 的 “缺失率” L2是核心独占如果 L2 共享核心 A 读取数据时会把核心 B 的缓存数据 “冲掉”导致双方频繁缺失效率反而下降。L3作为 “共享大池”服务多核心协做DRAM究竟慢在哪里从存储单元结构来说DRAM是有晶体管和电容组成电容漏电需要 “周期性刷新”。刷新占用 “读写时间窗口”导致访问排队同时刷新导致 “数据不一致”需要额外 “重写” 操作。从读写机制来说DRAM 的存储单元是 “行列矩阵”类似 Excel 表格不像 SRAM 那样可以 “直接定位地址读写”而是需要“先激活行→再选择列”的两步操作每一步都有固定的时序延迟累积起来就是 DRAM 的核心延迟来源。从物理布局来看普通 DRAMDDR5/DDR4是独立的 “内存条”通过主板上的 “内存插槽” 连接 CPU物理距离达厘米级 —— 信号在电路板上传输的速度约为光速的 50%1.5×10^8 m/s厘米级距离的传输延迟约1-5ns看似不大但叠加前面的读写延迟会进一步拉大与 SRAM 的差距内存访问机制带宽和延迟内存访问流程地址生成cpu处理指令的时候会根据指令需求生成内存地址这一过程涉及程序计数器PC的递增、指令译码以及操作数地址的计算。例如在执行加载Load指令时处理器根据指令中的寻址方式结合寄存器中的数据计算出要访问的内存单元地址。若为直接寻址指令中直接包含内存地址若是间接寻址则寄存器中的内容作为内存地址的指针 。地址传输与译码生成的内存地址通过地址总线传输到内存控制器。内存控制器中的地址译码器对地址进行解析将其分为行地址和列地址。以DRAM为例行地址用于选择存储阵列中的某一行打开相应的行缓冲器之后列地址再从行缓冲器中选择具体的列确定要访问的存储单元 。数据读写在读取操作中确定存储单元后数据从存储单元传输到数据缓冲器再通过数据总线返回处理器。写入操作时处理器将数据通过数据总线发送到内存的数据缓冲器然后写入指定的存储单元。整个读写过程受控制信号如读/写信号、片选信号等的协调与控制确保操作顺序的正确性 。内存延迟内存延迟指从处理器发出内存访问请求到收到数据的时间间隔通常以纳秒ns为单位度量。常见的内存延迟指标包括CAS延迟CLCAS Latency、RAS到CAS延迟tRCDRAS to CAS Delay、预充电延迟tRPRow Precharge Delay等。CL是指内存接收到读取命令后到数据开始输出的时钟周期数tRCD是行地址选通RAS信号与列地址选通CAS信号之间的延迟tRP则是内存行缓冲器关闭并进行预充电操作所需的时间 。补充一个 DRAM 芯片包含多个 “存储体Bank”每个 Bank 是一个二维矩阵矩阵的 “行” 叫RASRow Address Strobe行地址选通“列” 叫CASColumn Address Strobe列地址选通 读取一个数据的核心流程是激活行打开某一行→ 读取列从行中选某一列→ 预充电关闭行为下一次激活做准备每个步骤之间必须等待固定时间即对应的延迟参数否则会导致数据错误 —— 这就像你打开抽屉激活行后必须等抽屉完全打开才能拿东西读列拿完后必须等抽屉关上预充电才能打开另一个抽屉。内存带宽内存带宽指单位时间内内存能够传输的数据量通常以GB/s为单位。其计算公式为带宽 数据传输速率 × 每次传输的数据位宽 / 8。例如DDR4内存的数据传输速率为3200MT/s兆传输每秒位宽为64位则其带宽 3200 × 64 / 8 25600MB/s 25.6GB/s 。CacheCache line是什么Cache Line缓存行是 Cache 的 “最小数据传输和存储单元”。Cache 不是按 “字节” 或 “单个变量” 来读写数据而是一般按 “64 字节主流大小的块” 来操作。- 当 CPU 需要从内存读取一个字节的数据时Cache 会自动把 “这个字节所在的 64 字节连续数据块” 一起从内存加载到 Cache 中这个 64 字节的块就是 1 个 Cache Line- 当 CPU 修改 Cache 中的某个数据时也是先修改 “该数据所在的 Cache Line”后续再通过 “写回机制” 将整个 Cache Line 同步回内存而非单独同步一个字节。缓存和内存之间的映射Cache 是比内存小得多的高速存储比如 L2 Cache 是 256KB内存是 16GB无法像内存那样 “每个地址对应一个位置”。因此需要一套规则把 “内存地址” 对应到 “Cache 中的位置”这套规则就是 “Cache 映射”描述一个cache需要cache的分级容量linesize以及每组的行个数。程序访问的局部性原理Locality局部性原理Principle of Locality描述计算机程序在执行时访问数据和指令的行为模式。局部性原理有两种主要形式时间局部性和空间局部性。局部性原理的核心 ——“CPU 访问数据不是随机的而是有规律的”就比如我们平时写论文的时候不会随机抽书一会儿翻《算法导论》一会儿翻《红楼梦》而是会 “集中看某几本相关的书比如全是 CNN 的资料”也会 “翻完一页后接着翻下一页不会跳着翻”—— 这就是 “局部性”。CPU 也一样访问数据时会 “集中访问某片区域的 data且短期内会重复访问某些 data”——Cache 正是利用这个规律只存 “CPU 近期会用的少量数据”就能大幅减少对慢内存的依赖。时间局部性Spatial指的是如果一个程序在某一时刻访问了某个数据项那么在不久的将来它可能会再次访问这个数据项。在上面的代码中sum变量就是一个体现时间局部性的例子。在计算数组元素的和时sum变量在每次迭代时都会被频繁地访问并更新。由于sum变量的值在每次迭代中都被反复读取和修改所以它展示了很强的时间局部性。空间局部性Temporal指的是如果一个程序在某一时刻访问了某个数据项那么它很可能会访问与这个数据项相邻的数据项。意思是如果一个程序访问了某个数据项那么在不久的将来可能不会再次访问这个数据项。在上面的代码中array数组的访问就是一个体现空间局部性的例子。当我们在循环中依次访问array[i]时由于数组在内存中是连续存储的所以访问array[i]之后程序很可能会访问array[i1]。这就是空间局部性的体现。如何通过局部性提高命中率加快数据加载无论是空间局部性还是时间局部性都遵循同一个底层因果链局部性生效 → Cache 命中率提高 → 减少慢内存访问 → 数据加载速度变快。空间局部性是靠提前加载邻居数据减少后续缺失提高命中率。空间局部性的核心贡献是 “通过批量加载相邻数据把‘多次缺失’变成‘一次缺失 多次命中’直接提高命中率”—— 命中率从 0% 升到 93.75%最终让加载速度从 0.13GB/s 升到 0.85GB/s快 6 倍多。时间局部性的核心是 “让‘近期会重复访问的数据’留在 Cache 中不被其他数据替换”—— 这样重复访问时不用再读内存直接命中从而提高命中率最终让加载速度变快。SIMD 单指令流多数据流一种 采用一个控制器来控制多个处理器同时对一组数据又称“数据向量”中的每一个分别执行相同的操作从而实现空间上的并行性的技术。SIMD 指令集通常基于“向量”或“宽寄存器”这些寄存器能够存储多个数据元素。比如Intel 的 AVXAdvanced Vector Extensions指令集就支持 256 位和 512 位的向量寄存器每个寄存器可以存储多个 32 位浮点数或整数。SIMD单指令多数据 │ ├── ARM 平台 │ └── NEON / Advanced SIMD │ ├── x86 平台 │ ├── SSE │ ├── AVX │ └── AVX2 / AVX-512 │ └── RISC-V 平台 └── RVVSIMD 用一条指令处理多个连续数据如何在 Windows PC 上查看 CPU 是否支持 SIMD下载CPU-Z查看连续内存c[i] a[i] b[i];这里的“连续”不是说 a、b、c 三个数组在内存中挨着放而是每一次循环访问的地址相对于上一次是“等步长递增”的。CPU看到的连续是load [base i * stride]在该循环中数组 a、b 和 c 的访问均为单位步长的线性访问内存地址连续且可预测能够很好地利用缓存和 SIMD 向量加载/存储指令因此属于 SIMD 友好的访问模式。SIMD 友好与否不取决于写没写 for 循环而取决于 CPU 能不能在“不知道未来”的情况下提前知道下一次会访问哪块内存。simd视角一次循环标量视角一次循环 处理 1 个 a[i]simd视角一次循环 同时处理 4 / 8 / 16 个 a[i]SIMD就是在CPU内部有一个超级宽的寄存器可以一次性装很多数据比如标量寄存器假设 32-bit[ a0 ]SIMD 寄存器AVX 256-bit [ a0 | a1 | a2 | a3 | a4 | a5 | a6 | a7 ]不是 8 个寄存器而是 1 个寄存器里有 8 个“格子”这些格子叫lane通道就是循环次数减少了八倍SIMD 不是多线程并行而是利用宽向量寄存器使一条指令在同一时刻对多个数据元素执行相同的运算相比标量执行每次只处理一个元素SIMD 每次可以处理多个元素。NEON标量和向量标量运算的本质是一条指令仅操作一个数据值对应 CPU 的 “标量运算单元数据维度标量是 “0 维” 的单一数值如 int8_t a 10、float b 3.14执行逻辑循环中逐一遍历数据每次只处理一个元素哪怕是简单的加减乘除也需要一条指令对应一个元素。NEON 数据类型和指令类型数据类型NEON 向量数据类型是根据以下模式命名的typesizexnumber_of_lanes_tegint8x16_t 是一个16 通道 的向量每个通道包含一个有符号 8 位整数uint8x16_t │ │ │ │ │ └── 16 个元素 │ └───── 每个元素 8 bit └──────── unsigned int无符号整数 所以 uint8x16_t 16 个 uint8_t 数据组成的向量 int16x8_t 8 个 int16_t 数据组成的向量 float32x4_t 4 个 float 数据组成的向量NEON 常用的是128-bit 向量寄存器所以刚好可以装16 个 uint8_t 16 × 8 128 bit 8 个 int16_t 8 × 16 128 bit 4 个 int32_t 4 × 32 128 bit 4 个 float 4 × 32 128 bit 2 个 double 2 × 64 128 bitNEON 还提供了数组向量数据类型x2_t, x3_t, x4_t最大支持 4 个命名模式如下typesizexnumber of lanesxlength of array_tegint8x16x4_t 是一个长度为 4 的数组每一个数据的类型为 int8x16_tD寄存器64-bitQ寄存器128-bit指令类型参数类型u是无符号s是有符号f是浮点指令简介数据读取指令顺序读取从内存中连续的地址读取同类型、同批次的数 据直接填充到单个 Neon 向量寄存器中 —— 是 Neon 最基础、最常用的加载方式假设内存中有连续的 4 个 float 值[1.0, 2.0, 3.0, 4.0]地址 0→地址 15128 位连续用vld1q_f32顺序读取float data[] {1.0f, 2.0f, 3.0f, 4.0f}; // 内存连续排布 float32x4_t vec vld1q_f32(data); // 顺序读取交织读取内存中的数据是多组交叉排布的比如 RGB 图像的R-G-B-R-G-B-...、立体声的左-右-左-右-...需要用 “多路加载指令”vld2/vld3/vld4把交叉数据拆分到多个向量寄存器中 ——“解交织” 后才能用 SIMD 并行运算。假设内存中是 RGB 像素的交织数据[R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4]每 3 个字节为一个像素交叉排布用vld3q_u8交织读取uint8_t rgb_data[] {R1,G1,B1, R2,G2,B2, R3,G3,B3, R4,G4,B4}; // 3路交织 uint8x16x3_t rgb_vecs vld3q_u8(rgb_data); // 交织读取3路#include arm_neon.h uint8_t rgb_result[12]; // 存储4个RGB像素4×312字节 uint8x16_t r_vec vdupq_n_u8(R1); // 示例R通道数据 uint8x16_t g_vec vdupq_n_u8(G1); // 示例G通道数据 uint8x16_t b_vec vdupq_n_u8(B1); // 示例B通道数据 // 构造交织存储的输入结构3路 uint8x16x3_t rgb_vecs {r_vec, g_vec, b_vec}; // 交织存储把3个寄存器的数据交叉写入内存 vst3q_u8(rgb_result, rgb_vecs);数据存储指令顺序存储把单个 Neon 向量寄存器中的连续同类型数据按内存地址递增的顺序无间隔地写回内存。假设向量寄存器vec中存着 4 个 float 运算结果[1.0, 2.0, 3.0, 4.0]用vst1q_f32顺序存储到内存数组#include arm_neon.h float result[4]; // 空数组用于接收结果 float32x4_t vec vdupq_n_f32(1.0f); // 示例寄存器填充1.0,2.0,3.0,4.0实际是运算结果 vec vaddq_f32(vec, vdupq_n_f32(0.0f)); // 模拟运算仅占位 // 顺序存储把寄存器数据连续写入内存 vst1q_f32(result, vec);交织存储把多个 Neon 向量寄存器中 “解交织” 的不同类数据如 R/G/B 通道、左 / 右声道按固定步长交叉写回内存NEON和cache的关系NEON 本身不存储数据所有运算数据必须从内存/ 寄存器读取。而 CPU 主存DDR的访问延迟是 几十到上百个时钟周期远高于 NEON 运算的1-2 个时钟周期—— 如果 NEON 直接从主存取数据会出现 “运算单元等数据” 的严重瓶颈即 内存墙。CacheL1/L2/L3作为 CPU 与主存之间的高速缓冲访问延迟仅1-10 个时钟周期是 NEON 高效运行的 “数据中转站”理想状态NEON 所需的数据已在 Cache 中直接读取并并行运算算力拉满糟糕状态NEON 所需的数据不在 Cache 中Cache Miss需等待主存加载此时 NEON 会空等加速效果大打折扣。数据流动路径主存DDR → L3 Cache共享 → L2 Cache核心独享 → L1 D-Cache核心独享 → NEON 寄存器 → NEON 运算单元 ↓ 主存DDR ← L3 Cache ← L2 Cache ← L1 D-Cache ← NEON 寄存器运算结果写回优化技巧数据布局优化使用16 字节对齐可通过 attribute((aligned(16))) 声明数组连续存储NEON 适合处理连续内存数据避免分散 / 跳跃访问如数组按行存储而非列存储测试实例代码yuv基础知识https://segmentfault.com/a/1190000042645482该链接中的demo 按照从简单到复杂的顺序分别验证了 NEON SIMD 在整数加法、浮点加法、浮点乘加、FMA 指令吞吐以及乱序内存访问场景下的性能表现用来理解 SIMD 加速不仅依赖计算指令也强烈依赖数据访问方式。BaiYun8/NEON-SIMDhttps://github.com/BaiYun8/NEON-SIMD本链接中代码实验围绕 BGR/RGB 到 YUV444 格式转换展开分别实现并对比了 OpenCV 库函数版本、C/C 标量计算版本以及 ARM NEON SIMD 优化版本。BaiYun8/RGB_YUV-Testhttps://github.com/BaiYun8/RGB_YUV-TestBGR 转 YUV固定的线性公式BT.6018bit Y 0.299R 0.587G 0.114B U -0.169R - 0.331G 0.500B 128 V 0.500R - 0.419G - 0.081B 128 由于U.V本来有负数但是为了存储范围在0-255要整体加128再将系数整体乘256去掉小数点公式如下 Y (R * 77 G * 150 B * 29) 8 U (B * 128 - R * 43 - G * 85) 8 128 V (R * 128 - G * 107 - B * 21) 8 128 U继续变形U (-43*R - 85*G 128*B 32768) 8 根据数据范围来看YUV数据范围为 Y【0,65280】 U【0,255】 V【0,255】 但是在计算过程中UV都可能出现-32640int16_t 最大 32767所以偏移量32678需要int32。 综上针对Y数据计算使用uint16类型针对UV数据计算使用uint32类型

相关新闻