第11章:优化 I/O
对齐第一版第11章 Optimize I/O:文件读取方案、克制签名与缩短调用链、减少扩容、增大读取粒度/缓冲和逐行读取、无效尝试、文件写入及 cin/cout 流式 I/O。
学习目标
- 能设计 file-reading recipe,明确 ownership/size/error/encoding,并缩短 open/read/copy 调用链
- 能比较 whole/buffered/line reading 与 buffer size,减少 reallocation/call 又控制 peak memory/latency
- 能修改 file/cin/cout writing/reading path,减少 format/flush,并记录 things that did not help
从 Consumer 需要什么开始
I/O optimization 不是先换 API,而是先确认 consumer 需要 whole file、line、token 还是 streaming event。读取可能受 storage latency、system boundary、copy/allocation、format/parse 或 downstream work 限制,只有 profile/counters 能区分。
↡按 consumer contract 一次完成 open、storage planning、read、partial/error/EOF handling 与 output ownership 的文件读取流程。Parsimonious Signature 与 Short Calling Chains
parsimonious function signature 只暴露必要 inputs/outputs:例如 path + owning result,或 stream/source + caller buffer/callback;不重复传 filename/file/size/temp buffers,也不让多个 wrappers 各复制同一内容。
↡只表达 I/O 必需 source、destination/owner、policy 和 error 的克制函数接口,使 allocation 与 copy 可见。shorten calling chains 删除 readFile -> readAll -> readChunks -> copyToString 这类只转发/重复验证的 hot chain;ownership、security、encoding、observability layer 有真实语义则保留。可提供一个 direct batch/whole-file path,而不是做 god function。
std::expected<std::string, ReadError> readTextFile(
const std::filesystem::path& path,
std::size_t maxBytes) {
std::ifstream input(path, std::ios::binary);
if (!input) return std::unexpected(ReadError::openFailed());
std::string text;
text.reserve(std::min(sizeHint(input).value_or(0), maxBytes));
readBounded(input, text, maxBytes);
if (input.bad()) return std::unexpected(ReadError::readFailed());
return text;
}示例把 owner/error/max size 放在签名;sizeHint 只作 hint,file 可能变化、stream 可能不可 seek。readBounded 必须正确处理 partial reads、EOF 与上限,不能按 hint 盲目 resize 后把未读 bytes 当有效 data。
Reduce Reallocation,Take Bigger Bites
reduce reallocation:已知可信 bounded size 时 reserve/resize-on-read;未知时复用 chunk/output capacity,并记录 growth。不要每 chunk result = result + chunk 形成 temporary,也不要为攻击者声明的巨大 length 直接 allocation。
take bigger bites 把逐 byte/small read 合并成 larger read,摊薄 function/stream/system boundary 与 state checks。bigger input buffer 也有 diminishing returns:超过 device/OS/cache 合理粒度后,只增加 memory 和 first-result latency。
↡每次读取更大连续 chunk,以更少调用和检查处理相同 bytes,同时受 memory 与 latency 约束。 ↡在 source 与 parser 之间复用较大 storage,减少 refill/call 次数;大小必须通过 workload sweep 选择。std::expected<void, ReadError> forEachLine(
std::istream& input,
const std::function<bool(std::string_view)>& consume,
std::size_t maxLineBytes) {
std::string line;
line.reserve(std::min<std::size_t>(4096, maxLineBytes));
while (std::getline(input, line)) {
if (line.size() > maxLineBytes) return std::unexpected(ReadError::lineTooLong());
if (!consume(line)) break;
}
if (input.bad()) return std::unexpected(ReadError::readFailed());
return {};
}read a line at a time 能 bounded incremental processing,不需 whole-file peak memory;但 single pathological line 仍可增长,需 max policy。std::function 可能带 type-erasure/indirect call;若 callback 位于热点可改 template,但先测 parse/I/O 是否主导。
Things That Didn't Help
原书保留 things that didn't help 很关键:optimization 是实验,不是只展示成功。常见无效尝试包括在 device-bound path 微调 character loop、buffer 已足够大后继续增大、增加一层 copying stream adapter、手写不如成熟 stream buffer 的代码,或 mmap/whole-file 在当前 workload 反而增加 page faults/memory。
↡经同环境 A/B 证明无稳定端到端收益、或 correctness/resource trade-off 不合格的优化假设。失败实验应保存在 lab notebook:hypothesis、change、samples、why rejected;代码则回滚,不留下“也许有用”的复杂分支。bottleneck shift 也不是失败:它说明下一步要重新 profile。
Writing Files
writing files 需区分 buffered visibility 与 durability。合并 format、复用 buffer、一次写 larger chunk、少 flush 能提高 throughput;但 close/flush/fsync 的 failure/durability contract 不可丢。只测 write call 返回前时间可能把成本推到 close 或 OS cache。
std::expected<void, WriteError> writeRecords(
std::ostream& output,
std::span<const Record> records) {
std::string buffer;
buffer.reserve(64 * 1024);
for (const Record& record : records) {
appendRecord(buffer, record);
if (buffer.size() >= 64 * 1024) {
output.write(buffer.data(), static_cast<std::streamsize>(buffer.size()));
buffer.clear();
if (!output) return std::unexpected(WriteError::failed());
}
}
output.write(buffer.data(), static_cast<std::streamsize>(buffer.size()));
return output ? std::expected<void, WriteError>{}
: std::unexpected(WriteError::failed());
}buffer size 需 sweep;clear 复用 capacity。若 record 必须一条即刻可见,batching 改变 latency/atomicity,需 flush deadline 或不同 API。binary/text encoding 与 newline policy也要固定。
Reading from std::cin,Writing to std::cout
streaming I/O 逐步生产/消费,适合 unknown/unbounded input。reading from std::cin 可在程序启动且不再混用 C stdio 时 std::ios::sync_with_stdio(false),并按 interactive contract决定 std::cin.tie(nullptr);untie 后 prompt 可能需要显式 flush。
writing to std::cout:std::endl 写 newline 并强制 flush,batch output 通常用 \n;interactive prompt、protocol boundary 或 crash visibility 需要 flush 时仍应显式做。formatting cost 可能高于 write,应批量 format/使用合适 formatter并校验 locale/precision。
三步优化 I/O
第一步:重建文件读取边界
先预测 bottleneck 在 device、boundary、allocation/copy、format/parse 哪层;以 parsimonious signature 一次 open,明确 owner/error/max/encoding 并缩短无价值调用链。
小结
- file read recipe 从 consumer 的 whole/line/token/stream contract 开始,不从 API 名称开始
- parsimonious signature 让 owner/size/error/encoding 可见,short call chain 删除重复 open/copy/validation
- reserve/reuse 减少 reallocation,take bigger bites/bigger buffer 摊薄调用但受 memory/latency 约束
- line-at-a-time 降低 whole-file peak,需要长行、newline、encoding 与 callback lifetime policy
- things that did not help 也要记录并回滚,避免无证据复杂度留在代码
- file writing 要同时测 format/write/flush/close 与 durability,不能把成本推迟后忽略
- cin/cout sync/tie/endl 优化必须保留 C stdio mixing 与 interactive flush 语义
名词解释
本章出现的专业名词,用大白话再讲一遍。
- recipe for reading files
从消费契约到 open/storage/read/error/output 的完整读取流程。
- parsimonious function signature
只暴露必要 source、owner/policy 与 error 的 I/O 接口。
- shorten calling chains
删除 I/O hotspot 中无语义价值的转发和重复工作。
- reduce I/O reallocation
用 size hint、reserve/reuse 或 chunk 避免 output 反复增长。
- take bigger bites
每次读取更大 chunk 以摊薄调用与检查。
- bigger input buffer
复用更大输入 storage 减少 refill/call。
- read a line at a time
按 line 增量读取消费并限制 whole-file ownership。
- things that did not help
A/B 无稳定收益或 trade-off 不合格的优化假设。
- streaming I/O
数据到达/生成时增量读写的模式。
练习
- 问题 1:优化 2 GiB 行日志分析。 当前 whole-file string + split copies,p99 memory 爆炸;要求首批结果 100 ms 内。给出 recipe。
- 问题 2:解释
sync_with_stdio(false)后交互提示不显示。 程序cout << prompt; cin >> x;且混用 printf。给出安全策略。
- 问题 3:评审“大 buffer 让写入快 8%”提案。 buffer 从 64 KiB 增到 64 MiB,32 并发 writers,durability 每秒一次。写出验收。