上一篇我们聊了写合并,实际情况中写文件、发网络包、填显存,如果逐字节/逐字段操作,性能会惨不忍睹。这一篇具体讲讲在业务代码中可能遇到的情况。


1. 核心思想:缓冲+批量

无论哪种语言,写合并的核心都是两个手段:

  1. 用户态缓冲:在内存里攒数据,满了再刷
  2. 批量操作:一次系统调用传输大块数据

代价是延迟增加(数据在缓冲区等),但吞吐量提升10-100倍。


2. Java:BufferedOutputStream不是银弹

Java的IO流设计体现了写合并思想,但用不好反而慢。

2.1 BufferedOutputStream原理

BufferedOutputStreamOutputStream上加了个8KB缓冲区1

// 默认缓冲区8KB
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("data.bin")
);

// 写操作先填缓冲区,满了再调FileOutputStream
bos.write(65);  // 只是填数组,不刷盘
bos.flush();    // 强制刷盘

2.2 实测:什么时候快,什么时候慢

脚本之家做过对比测试2,结果出人意料:

测试1:逐字节写入(5秒)

// 每次写1个字节
while (running) {
    bos.write(65);  // BufferedOutputStream
}
// 结果:300MB

while (running) {
    fos.write(65);  // FileOutputStream
}
// 结果:1.71MB

BufferedOutputStream快175倍

测试2:批量写入80KB(5秒)

byte[] buffer = new byte[81920];

while (running) {
    bos.write(buffer);  // BufferedOutputStream
}
// 结果:4.5GB

while (running) {
    fos.write(buffer);  // FileOutputStream
}
// 结果:6.87GB

FileOutputStream反而快50%

2.3 结论

场景推荐方案原因
小数据频繁写BufferedOutputStream减少系统调用次数
大数据批量写FileOutputStream + 大数组避免二次拷贝
网络IOBufferedOutputStream减少包数量,降低延迟

Wisconsin大学的研究3也证实:直接缓冲(自己维护数组)比BufferedOutputStream再快40%,因为没有Java层的额外拷贝。

2.4 最佳实践

// 方案1:自己缓冲(最快)
byte[] buffer = new byte[65536];  // 64KB
int pos = 0;

for (Record r : records) {
    byte[] data = r.serialize();
    if (pos + data.length > buffer.length) {
        fos.write(buffer, 0, pos);  // 批量刷
        pos = 0;
    }
    System.arraycopy(data, 0, buffer, pos, data.length);
    pos += data.length;
}
if (pos > 0) fos.write(buffer, 0, pos);

// 方案2:NIO的MappedByteBuffer(零拷贝)
FileChannel channel = new RandomAccessFile("data.bin", "rw").getChannel();
MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, fileSize);
// 直接写内存,由OS异步刷盘

3. Python:memoryview实现零拷贝

Python的写合并优化依赖缓冲协议和memoryview45

3.1 问题:bytes是不可变的

# 每次拼接都创建新对象,O(n²)复杂度
data = b''
for i in range(10000):
    data += b'x' * 100  # 每次都要分配新内存、拷贝

3.2 方案1:bytearray + memoryview

# 预分配缓冲区
buf = bytearray(65536)  # 64KB
mv = memoryview(buf)    # 零拷贝视图
pos = 0

for record in records:
    data = record.encode()
    if pos + len(data) > len(buf):
        f.write(buf[:pos])  # 批量写
        pos = 0
    mv[pos:pos+len(data)] = data  # 零拷贝填充
    pos += len(data)

if pos > 0:
    f.write(buf[:pos])

memoryview的关键特性67

  • 不拷贝数据,只是引用底层buffer
  • 支持切片(也是零拷贝)
  • 可读写(如果底层buffer可变)

3.3 方案2:struct直接pack到buffer

import struct
import ctypes

# 预分配buffer
buf = ctypes.create_string_buffer(1024)

# 直接pack到指定位置,零拷贝
struct.pack_into('!HHI', buf, 0, 0x1234, 0x5678, 0xABCDEF00)  # header
struct.pack_into('!20s', buf, 8, b'hello world')  # payload

# 一次性写入
with open('data.bin', 'wb') as f:
    f.write(buf[:28])

struct.pack_into避免了临时bytes对象的创建89

3.4 方案3:numpy的ascontiguousarray

处理numpy数组时,非连续内存会导致性能问题10

import numpy as np

# 非连续数组(切片产生)
arr = np.arange(100)[::2]  # 步长2,非连续

# 错误:BufferError
mv = memoryview(arr)  # 可能失败

# 正确:先转连续
arr_contig = np.ascontiguousarray(arr)
mv = memoryview(arr_contig)  # 成功

3.5 性能对比

方法1万条记录耗时内存占用
bytes +=2.5s高(频繁分配)
bytearray 预分配 + 切片 (3.2)0.25s低(零拷贝)
struct.pack_into (3.3)0.15s低(零拷贝)

4. C++:从std::ostream到系统调用

C++的IO库分层复杂,写合并优化需要穿透多层抽象。

4.1 std::ostream的问题

// 每次<<都可能导致虚函数调用和格式化
std::ofstream ofs("data.txt");
ofs << x << "," << y << "," << z << "\n";  // 慢

std::ostream的问题:

  1. 虚函数开销(多态)
  2. 每次操作都检查流状态
  3. 默认不缓冲(或行缓冲)

4.2 方案1:手动缓冲

#include <fstream>
#include <vector>

class BufferedWriter {
    std::ofstream& out;
    std::vector<char> buf;
    size_t pos = 0;

public:
    BufferedWriter(std::ofstream& o, size_t size = 65536) 
        : out(o), buf(size) {}

    void write(const char* data, size_t len) {
        if (pos + len > buf.size()) {
            flush();
        }
        memcpy(buf.data() + pos, data, len);
        pos += len;
    }

    void flush() {
        if (pos > 0) {
            out.write(buf.data(), pos);
            pos = 0;
        }
    }

    ~BufferedWriter() { flush(); }
};

// 使用
std::ofstream ofs("data.bin", std::ios::binary);
BufferedWriter writer(ofs);

for (const auto& record : records) {
    writer.write(record.data(), record.size());
}

4.3 方案2:mmap零拷贝

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
ftruncate(fd, fileSize);

// 映射文件到内存
char* map = (char*)mmap(nullptr, fileSize, PROT_WRITE, MAP_SHARED, fd, 0);

// 直接写内存,OS异步刷盘
size_t pos = 0;
for (const auto& record : records) {
    memcpy(map + pos, record.data(), record.size());
    pos += record.size();
}

msync(map, fileSize, MS_ASYNC);  // 异步刷盘
munmap(map, fileSize);
close(fd);

4.4 方案3:writev批量scatter-gather

Linux的writev系统调用可以一次写多个不连续的buffer:

#include <sys/uio.h>

struct iovec iov[3];
iov[0].iov_base = header;
iov[0].iov_len = header_len;
iov[1].iov_base = payload1;
iov[1].iov_len = payload1_len;
iov[2].iov_base = payload2;
iov[2].iov_len = payload2_len;

// 一次系统调用写3个buffer
writev(fd, iov, 3);

这比3次write系统调用快,因为减少了用户态/内核态切换。


5. 跨语言通用优化原则

原则JavaPythonC++
减少系统调用BufferedOutputStream批量writewritev/mmap
避免内存拷贝直接ByteBuffermemoryviewmmap/writev
预分配大buffernew byte[65536]bytearray(65536)vector
批量序列化ByteBuffer.putstruct.pack_intomemcpy
异步刷盘NIO后台线程msync(MS_ASYNC)

6. 总结

三种语言的写合并优化思路相通,但实现不同:

语言关键工具核心优化
JavaBufferedOutputStream, ByteBuffer, MappedByteBuffer减少系统调用,避免JNI拷贝
Pythonbytearray, memoryview, struct.pack_into零拷贝视图,直接buffer操作
C++手动buffer, mmap, writev穿透抽象,直接系统调用

关键认知

  1. 缓冲不是万能的,大数据批量写反而要避免二次缓冲
  2. 零拷贝(memoryview/mmap)比缓冲更快,但代码更复杂
  3. 系统调用次数是瓶颈,每次write都有上下文切换开销

理解这些,写高性能IO代码时就能做出正确选择。

参考


  1. TechVidvan. Java BufferedOutputStream Class with Examples. ↩︎

  2. 脚本之家. Java中IO流的BufferedOutputStream和FileOutputStream对比. ↩︎

  3. University of Wisconsin. An In-Depth Examination of Java I/O Performance. ↩︎

  4. CSDN博客. Python内置函数memoryview()详解. ↩︎

  5. CSDN博客. python内置类memoryview()详解. ↩︎

  6. Arjan Codes. Efficient Python Data Handling with MemoryView. ↩︎

  7. Codecademy. Python | Built-in Functions | memoryview(). ↩︎

  8. GeeksforGeeks. struct module in Python. ↩︎

  9. DigitalOcean. Python struct pack, unpack. ↩︎

  10. Sling Academy. NumPy BufferError – memoryview: underlying buffer is not C-contiguous. ↩︎

更多推荐