CPU 应该 搞 0 级 Cache , 而不是 大寄存器 。
具体的说, 是 CPU 应该 搞 精简指令集 RISC 和 0 级 Cache , 而 不是 大寄存器 。
0 级 Cache 也可以称为 L0 Cache 。
0 级 Cache 是 离 CPU 最近 的 Cache, 访问 只需要 1 个 时钟周期, 和 寄存器 一样 。
那 0 级 Cache 和 寄存器 有 什么 区别 呢 ?
0 级 Cache 在 内存 地址编制 内, 和 一级 Cache 、二级 Cache 、三级 Cache 、内存 在 一个 统一 的 地址空间 里, 按 统一 的 地址管理 。
而 寄存器 是 不在 内存 地址编制 里 的 。
0 级 Cache 从 下级存储 (一级 Cache 、二级 Cache 、三级 Cache 、内存) 载入载出 哪些 数据 是 完全 由 程序员 控制 的, 具体的, 是 完全 由 程序员用 汇编指令控制的 。
这是 和 一级 Cache 、二级 Cache 、三级 Cache 的 不同 。
一级 Cache 、二级 Cache 、三级 Cache 载入载出 哪些 数据 是由 CPU 自己决定的, 比如 根据 命中算法, 程序员 无权干涉 。
程序员 用 指令 map_in 将 内存(Cache) 地址 和 数据 映射 进 0 级 Cache , 如果 0 级 Cache 里 的 存储单元 原来已经 映射 了 地址 和 数据, 此时 将 新的 地址 映射 到 这个 存储单元, 则 旧 的 数据 将 替换 为 新 的 数据, 旧 的 映射地址 将 映射 成 新 的 地址, 如果 旧 的 数据 被 修改过, 则 要 先 写回 对应 的 内存(Cache) 地址 , 这 称为 map_out , 也可以称为 载出 。
map_in 也可以 称为 载入 。
0 级 Cache 的 好处 是 :
1 指针取值( * 指针) 和 指针字段( 指针 --> 字段 ) 可以 享有 和 局部变量 一样 的 寄存器优化 的 待遇
寄存器优化 就是 把 常用 的 数据 存在 寄存器 里 反复使用 。
在 寄存器 架构下, 指针取值( * 指针) 和 指针字段( 指针 --> 字段 ) 不容易 做 寄存器优化, 因为 指针 会 改变, * 指针 和 指针 --> 字段 会 随 指针 的 改变 而 改变,
同时, * 指针 和 指针 --> 字段 可能 被 其它 同样指向这个 地址 的 指针 修改, 比如 指针2 和 指针 相等, * 指针2 和 指针 -> 字段 修改 的 数据 就是 * 指针 和 指针 --> 字段 的 数据, 但是 * 指针 和 指针 --> 字段 并不知道 数据 被 修改 。
这还只是 单线程 的 情况 。
多线程 也会 造成 类似 的 数据 不一致 的 情况 。
但 使用 0 级 Cache 的 话, 0 级 Cache 是 按 地址 访问 的, 和 一级 Cache 、二级 Cache 、三级 Cache 、内存 同在一个 地址编制 , 对于 指针取值( * 指针) 和 指针字段( 指针 --> 字段 ), 都是 按 地址访问, 不用 担心 数据不一致 的 问题 。 而 访问 0 级 Cache 的 时间 是 1 个 时钟周期, 和 寄存器 一样快 。
2 多核 数据同步 和 单核多线程 并发 数据一致
这 其实 是 第 1 点 里 说 的 多线程 的 情况, 对于 多核 的 共享数据, 修改 时 要 mutex 并 同步到 各 核 的 Cache, 在 寄存器 架构下, 对于 需要 实时同步 的 多核数据, 是 不能 做 寄存器优化 的, 也就是 要 禁用 寄存器优化, 比如 C++ 里 的 atomic<T> 原子类型 是 禁用 寄存器 优化 的 。
而 现在 用 0 级 Cache, 就不存在这个问题, 0 级 Cache 和 现在 的 1 级 Cache 一样, 修改 原子数据 时 直接 mutex 和 通知 其它 核 同步,
这样 会不会 影响性能 ?
不会 。 读取 时 仍然 是 1 个 时钟周期 , 修改 时 会 发起 mutex , mutex 要 通知 到 其它 核 , 当然 需要 一些 的 时钟周期, 另外, 若 收到 其它 核 已 改写数据 的 通知, 要从 其它 核 的 Cache 里 把 数据 同步过来, 这 也要 一些 时钟周期 。
除此以外, 读取 时 是 1 个 时钟周期 。 也就是说, 如果 自己 不改写, 也 没有 收到 其它 核 改写 的 通知, 读取 0 级 Cache 里 的 原子变量 是 1 个 时钟周期, 和 普通变量 一样 。
对于 单核多线程 并发 共享数据, 要 保证 数据 在 并发中一致, 也要 禁用 寄存器优化, 同理, 用 0 级 Cache, 就不存在这个问题 。
3 编译器 / 程序员 不用 考虑 把 寄存器 里 的 数据 写回 Cache / 内存
在 寄存器 架构下, 常用 的 数据 存在 寄存器 里 反复使用 , 用完后(比如 函数 结束时), 如果 数据 被 修改 过, 要 写回 Cache / 内存 ,
用 0 级 Cache 就 不用 编译器 / 程序员 考虑 这件事 了 。
0 级 Cache 会 记录 哪些 数据 被 修改过, 被 修改 的 才 写回 映射 的 内存地址(当然, 实际上 可能 是 写 Cache , 也可能写 内存),
这 需要 0 级 Cache 的 硬件电路 将 被 修改过的 存储单元 标记 为 “被修改” ,
对于 这一点, 硬件电路 很容易 做到 。
事实上, 在 0 级 Cache 里, 程序员 也不用 考虑 在 什么 “时机” 把 数据 写回 一级 Cache ( 二级 Cache 、三级 Cache 、内存 ),
因为 0 级 Cache 也是 Cache, 和 一级 Cache 、二级 Cache 、三级 Cache 、内存 本身 就是 一个 体系 ,
就好像 程序员 不用 考虑 一级 Cache 的 数据 “写回” 二级 Cache 、三级 Cache 、内存 。
“时机” 比如 上面说的 “用完后(比如 函数 结束时)” , 在 0 级 Cache 里, 程序员 也不用 考虑 这些 。
程序员 只要 考虑 把 哪个 (需要的) 地址 映射 到 0 级 Cache 的 哪个 存储单元, 这个 存储单元 原来 的 数据 如果 修改过的话, 会 自动写回 映射 的 地址 ( 一级 Cache 、二级 Cache 、三级 Cache 、内存 ) 。
4 Localize 指针访问 一个周期
InitedLength 原子变量 Add() 时 改变 Local Agent
map out 硬件电路 容易做到
我提倡 用 模块线路图 来 设计 硬件电路, 硬件电路 本来 就是 模块化 的 , 用 模块线路图 设计 很适合 。 模块 的 规格, 包括 接口 和 电路参数 作为 模块 的 说明书 单独说明 就好 。
其实 设计 CPU 很简单, 主要 是 制造工艺 和 电路计算 比较难 。