第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 能区分。

Parsimonious Signature 与 Short Calling Chains

parsimonious function signature 只暴露必要 inputs/outputs:例如 path + owning result,或 stream/source + caller buffer/callback;不重复传 filename/file/size/temp buffers,也不让多个 wrappers 各复制同一内容。

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。

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。

失败实验应保存在 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::coutstd::endl 写 newline 并强制 flush,batch output 通常用 \n;interactive prompt、protocol boundary 或 crash visibility 需要 flush 时仍应显式做。formatting cost 可能高于 write,应批量 format/使用合适 formatter并校验 locale/precision。

三步优化 I/O

分步1 / 3

第一步:重建文件读取边界

先预测 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 语义

资料与写作方式声明

本章以Optimized C++, First Edition, Chapter 11: Optimize I/O权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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. 问题 1:优化 2 GiB 行日志分析。 当前 whole-file string + split copies,p99 memory 爆炸;要求首批结果 100 ms 内。给出 recipe。
  1. 问题 2:解释 sync_with_stdio(false) 后交互提示不显示。 程序 cout << prompt; cin >> x; 且混用 printf。给出安全策略。
  1. 问题 3:评审“大 buffer 让写入快 8%”提案。 buffer 从 64 KiB 增到 64 MiB,32 并发 writers,durability 每秒一次。写出验收。

讨论

评论区加载中…