C++/Java/Python写合并优化实战
上一篇我们聊了写合并,实际情况中写文件、发网络包、填显存,如果逐字节/逐字段操作,性能会惨不忍睹。这一篇具体讲讲在业务代码中可能遇到的情况。
1. 核心思想:缓冲+批量
无论哪种语言,写合并的核心都是两个手段:
- 用户态缓冲:在内存里攒数据,满了再刷
- 批量操作:一次系统调用传输大块数据
代价是延迟增加(数据在缓冲区等),但吞吐量提升10-100倍。
2. Java:BufferedOutputStream不是银弹
Java的IO流设计体现了写合并思想,但用不好反而慢。
2.1 BufferedOutputStream原理
BufferedOutputStream在OutputStream上加了个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 + 大数组 | 避免二次拷贝 |
| 网络IO | BufferedOutputStream | 减少包数量,降低延迟 |
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])
- 不拷贝数据,只是引用底层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的问题:
- 虚函数开销(多态)
- 每次操作都检查流状态
- 默认不缓冲(或行缓冲)
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. 跨语言通用优化原则
| 原则 | Java | Python | C++ |
|---|---|---|---|
| 减少系统调用 | BufferedOutputStream | 批量write | writev/mmap |
| 避免内存拷贝 | 直接ByteBuffer | memoryview | mmap/writev |
| 预分配大buffer | new byte[65536] | bytearray(65536) | vector |
| 批量序列化 | ByteBuffer.put | struct.pack_into | memcpy |
| 异步刷盘 | NIO | 后台线程 | msync(MS_ASYNC) |
6. 总结
三种语言的写合并优化思路相通,但实现不同:
| 语言 | 关键工具 | 核心优化 |
|---|---|---|
| Java | BufferedOutputStream, ByteBuffer, MappedByteBuffer | 减少系统调用,避免JNI拷贝 |
| Python | bytearray, memoryview, struct.pack_into | 零拷贝视图,直接buffer操作 |
| C++ | 手动buffer, mmap, writev | 穿透抽象,直接系统调用 |
关键认知:
- 缓冲不是万能的,大数据批量写反而要避免二次缓冲
- 零拷贝(memoryview/mmap)比缓冲更快,但代码更复杂
- 系统调用次数是瓶颈,每次write都有上下文切换开销
理解这些,写高性能IO代码时就能做出正确选择。
参考
TechVidvan. Java BufferedOutputStream Class with Examples. ↩︎
脚本之家. Java中IO流的BufferedOutputStream和FileOutputStream对比. ↩︎
University of Wisconsin. An In-Depth Examination of Java I/O Performance. ↩︎
CSDN博客. Python内置函数memoryview()详解. ↩︎
CSDN博客. python内置类memoryview()详解. ↩︎
Arjan Codes. Efficient Python Data Handling with MemoryView. ↩︎
Codecademy. Python | Built-in Functions | memoryview(). ↩︎
GeeksforGeeks. struct module in Python. ↩︎
DigitalOcean. Python struct pack, unpack. ↩︎
Sling Academy. NumPy BufferError – memoryview: underlying buffer is not C-contiguous. ↩︎
更多推荐
所有评论(0)