七、存储与I/O
7.1 机械硬盘的物理特性
/*
* HDD寻道时间
*
* 访问延迟 = 寻道时间 + 旋转延迟 + 传输时间
*
* - 寻道时间(Seek Time):磁头移动到目标磁道,~5-10ms
* - 旋转延迟(Rotational Latency):扇区旋转到磁头下,7200rpm ≈ 4.17ms
* - 传输时间:取决于密度和接口速度,通常很小
*
* 机械硬盘随机访问:~10ms
* 顺序访问:取决于密度,~200MB/s
*/
// 测试磁盘性能
# 测试随机读写
fio --name=randread --rw=randread --bs=4k --size=1G --numjobs=4 --runtime=60
# 测试顺序读写
fio --name=seqread --rw=read --bs=1M --size=1G --numjobs=1 --runtime=60
7.2 SSD工作原理
/*
* NAND Flash存储单元
*
* SLC: 1位/单元,寿命长(~10万次擦写),速度快
* MLC: 2位/单元,寿命中等(~1万次擦写)
* TLC: 3位/单元,寿命较短(~3000次擦写)
* QLC: 4位/单元,寿命短(~1000次擦写)
*
* 写入放大(Write Amplification):
* SSD的最小写入单位是页(4KB),但擦除单位是块(2-4MB)
* 修改4KB数据可能需要读取整个块、擦除、重写
*
* 优化建议:
* 1. 使用TRIM命令通知SSD哪些数据已删除
* 2. 对齐分区到擦除块边界(4K对齐)
* 3. 避免频繁小数据写入
* 4. 使用NVMe协议替代AHCI
*/
// 查看SSD信息
smartctl -a /dev/nvme0n1
7.3 I/O栈与零拷贝
/*
* Linux I/O栈
*
* 用户空间
* ↓ read()/write()系统调用
* VFS(虚拟文件系统)
* ↓
* 具体文件系统(ext4, xfs等)
* ↓
* 页缓存(Page Cache)
* ↓
* 块层(Block Layer)
* ↓
* I/O调度器(mq-deadline, kyber等)
* ↓
* 块设备驱动
* ↓
* 物理磁盘
*/
/*
* 零拷贝技术
*
* 传统文件传输流程(4次拷贝,4次上下文切换):
* 磁盘 → 内核缓冲区(read) → 用户缓冲区 → 内核Socket缓冲区(write) → 网卡
*
* sendfile零拷贝(2次拷贝,2次上下文切换):
* 磁盘 → 内核缓冲区 → 内核Socket缓冲区 → 网卡
*
* 代码示例:
*/
#include <sys/sendfile.h>
// 传统拷贝
void traditional_copy(const char* src, const char* dst) {
char buf[8192];
int in_fd = open(src, O_RDONLY);
int out_fd = open(dst, O_WRONLY | O_CREAT);
ssize_t bytes;
while ((bytes = read(in_fd, buf, sizeof(buf))) > 0) {
write(out_fd, buf, bytes);
}
}
// 零拷贝(Linux)
void zero_copy(const char* src, const char* dst) {
int in_fd = open(src, O_RDONLY);
int out_fd = open(dst, O_WRONLY | O_CREAT);
struct stat stat;
fstat(in_fd, &stat);
// 零拷贝传输
sendfile(out_fd, in_fd, 0, stat.st_size);
}
// Java零拷贝
public void zeroCopy(String from, String to) throws IOException {
FileChannel fromChannel = new FileInputStream(from).getChannel();
FileChannel toChannel = new FileOutputStream(to).getChannel();
fromChannel.transferTo(0, fromChannel.size(), toChannel);
}
八、性能的物理极限
8.1 CPU的功耗墙
/*
* Dennard Scaling(丹纳德缩放)已失效
*
* 2006年前:晶体管缩小,功耗同比例下降
* 2006年后:漏电流增加,功耗密度不再下降
*
* 结论:单个核心频率无法持续提升(当前稳定在3-5GHz)
* 解决:多核架构 + 专用加速器(GPU, TPU, NPU)
*
* 功耗公式:
* P = C × V² × f
* C: 电容, V: 电压, f: 频率
*/
// 查看CPU功耗
// 使用powertop
powertop
// 查看CPU频率
cat /proc/cpuinfo | grep MHz
cpupower frequency-info
8.2 内存墙
/*
* 处理器与内存的性能差距不断扩大
*
* CPU每1ns可以执行多条指令
* 内存访问需要80-100ns
*
* 比例:CPU速度增长 >> 内存速度增长
*
* 解决方案:
* - 多级缓存(L1/L2/L3)
* - 预取(Prefetching)
* - 内存控制器集成到CPU
* - HBM(High Bandwidth Memory)和CXL
*/
// 测量内存访问延迟
// 使用lmbench
lat_mem_rd -t 16M
8.3 Amdahl定律与可扩展性
/*
* Amdahl's Law(阿姆达尔定律)
*
* 加速比 = 1 / (串行比例 + 并行比例 / 核心数)
*
* 示例:如果程序有10%的串行代码,最多加速10倍(无论多少核心)
*/
// 计算并行效率
double speedup = 1.0 / (serial_ratio + (1 - serial_ratio) / cores);
double efficiency = speedup / cores;
// 结论:优化串行部分往往比增加核心更有效
九、底层原理在编程中的应用
9.1 利用CPU缓存优化
// 缓存友好的数据结构设计
public class CacheFriendlyQueue {
// 使用环形缓冲区,避免链表的内存跳跃
private static final int CAPACITY = 1024;
private final long[] buffer = new long[CAPACITY];
private int head = 0;
private int tail = 0;
// 预取提示(Java 9+)
public void add(long value) {
buffer[tail] = value;
// 预取下一个位置
Unsafe.getUnsafe().prefetchWrite(buffer, tail + 1);
tail = (tail + 1) & (CAPACITY - 1);
}
}
9.2 避免伪共享
// 使用缓存行对齐的计数器
public class Counter {
// 确保每个计数器在独立的缓存行
@sun.misc.Contended
static class PaddedCounter {
long value;
}
private final PaddedCounter[] counters;
public Counter(int threadCount) {
counters = new PaddedCounter[threadCount];
for (int i = 0; i < threadCount; i++) {
counters[i] = new PaddedCounter();
}
}
public void increment(int threadId) {
counters[threadId].value++;
}
public long sum() {
long total = 0;
for (PaddedCounter c : counters) {
total += c.value;
}
return total;
}
}
9.3 分支预测友好的代码
// 坏例子:随机顺序导致分支预测失败
int data[N];
qsort(data, N, sizeof(int), cmp); // 排序前
int sum = 0;
for (int i = 0; i < N; i++) {
if (data[i] > 128) { // 分支高度不可预测
sum += data[i];
}
}
// 好例子:排序后分支预测成功
qsort(data, N, sizeof(int), cmp); // 排序后
int sum = 0;
for (int i = 0; i < N; i++) {
if (data[i] > 128) { // 数据有序,分支稳定
sum += data[i];
}
}
// 更好:使用无分支代码
int sum = 0;
for (int i = 0; i < N; i++) {
sum += data[i] * (data[i] > 128); // 三元运算符也可能编译为条件跳转
// 使用位运算实现无分支
// int mask = -(data[i] > 128);
// sum += data[i] & mask;
}
9.4 SIMD编程示例
#include <immintrin.h>
// 使用AVX2向量化指令(一次处理8个float)
void vector_add(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i += 8) {
// 加载256位向量(8个float)
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
// 向量加法
__m256 vc = _mm256_add_ps(va, vb);
// 存储结果
_mm256_storeu_ps(&c[i], vc);
}
}