上一篇我們聊了寫合并,實際情況中寫文件、發(fā)網(wǎng)絡(luò)包、填顯存,如果逐字節(jié)/逐字段操作,性能會慘不忍睹。這一篇具體講講在業(yè)務(wù)代碼中可能遇到的情況。
1. 核心思想:緩沖+批量
無論哪種語言,寫合并的核心都是兩個手段:
- 用戶態(tài)緩沖:在內(nèi)存里攢數(shù)據(jù),滿了再刷
- 批量操作:一次系統(tǒng)調(diào)用傳輸大塊數(shù)據(jù)
代價是延遲增加(數(shù)據(jù)在緩沖區(qū)等),但吞吐量提升10-100倍。
2. Java:BufferedOutputStream不是銀彈
Java的IO流設(shè)計體現(xiàn)了寫合并思想,但用不好反而慢。
2.1 BufferedOutputStream原理
BufferedOutputStream在OutputStream上加了個8KB緩沖區(qū)[1]:
// 默認(rèn)緩沖區(qū)8KB
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("data.bin")
);
// 寫操作先填緩沖區(qū),滿了再調(diào)FileOutputStream
bos.write(65); // 只是填數(shù)組,不刷盤
bos.flush(); // 強(qiáng)制刷盤
2.2 實測:什么時候快,什么時候慢
腳本之家做過對比測試[2],結(jié)果出人意料:
測試1:逐字節(jié)寫入(5秒)
// 每次寫1個字節(jié)
while (running) {
bos.write(65); // BufferedOutputStream
}
// 結(jié)果:300MB
while (running) {
fos.write(65); // FileOutputStream
}
// 結(jié)果:1.71MB
BufferedOutputStream快175倍。
測試2:批量寫入80KB(5秒)
byte[] buffer = new byte[81920];
while (running) {
bos.write(buffer); // BufferedOutputStream
}
// 結(jié)果:4.5GB
while (running) {
fos.write(buffer); // FileOutputStream
}
// 結(jié)果:6.87GB
FileOutputStream反而快50%。
2.3 結(jié)論
| 場景 | 推薦方案 | 原因 |
|---|---|---|
| 小數(shù)據(jù)頻繁寫 | BufferedOutputStream | 減少系統(tǒng)調(diào)用次數(shù) |
| 大數(shù)據(jù)批量寫 | FileOutputStream + 大數(shù)組 | 避免二次拷貝 |
| 網(wǎng)絡(luò)IO | BufferedOutputStream | 減少包數(shù)量,降低延遲 |
Wisconsin大學(xué)的研究[3]也證實:直接緩沖(自己維護(hù)數(shù)組)比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);
// 直接寫內(nèi)存,由OS異步刷盤
3. Python:memoryview實現(xiàn)零拷貝
Python的寫合并優(yōu)化依賴緩沖協(xié)議和memoryview[4][5]。
3.1 問題:bytes是不可變的
# 每次拼接都創(chuàng)建新對象,O(n2)復(fù)雜度
data = b''
for i in range(10000):
data += b'x' * 100 # 每次都要分配新內(nèi)存、拷貝
3.2 方案1:bytearray + memoryview
# 預(yù)分配緩沖區(qū)
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])
- 不拷貝數(shù)據(jù),只是引用底層buffer
- 支持切片(也是零拷貝)
- 可讀寫(如果底層buffer可變)
3.3 方案2:struct直接pack到buffer
import struct
import ctypes
# 預(yù)分配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對象的創(chuàng)建[8][9]。
3.4 方案3:numpy的ascontiguousarray
處理numpy數(shù)組時,非連續(xù)內(nèi)存會導(dǎo)致性能問題[10]:
import numpy as np
# 非連續(xù)數(shù)組(切片產(chǎn)生)
arr = np.arange(100)[::2] # 步長2,非連續(xù)
# 錯誤:BufferError
mv = memoryview(arr) # 可能失敗
# 正確:先轉(zhuǎn)連續(xù)
arr_contig = np.ascontiguousarray(arr)
mv = memoryview(arr_contig) # 成功
3.5 性能對比
| 方法 | 1萬條記錄耗時 | 內(nèi)存占用 |
|---|---|---|
| bytes += | 2.5s | 高(頻繁分配) |
| bytearray 預(yù)分配 + 切片 (3.2) | 0.25s | 低(零拷貝) |
| struct.pack_into (3.3) | 0.15s | 低(零拷貝) |
4. C++:從std::ostream到系統(tǒng)調(diào)用
C++的IO庫分層復(fù)雜,寫合并優(yōu)化需要穿透多層抽象。
4.1 std::ostream的問題
// 每次<<都可能導(dǎo)致虛函數(shù)調(diào)用和格式化
std::ofstream ofs("data.txt");
ofs << x << "," << y << "," << z << "\n"; // 慢
std::ostream的問題:
- 虛函數(shù)開銷(多態(tài))
- 每次操作都檢查流狀態(tài)
- 默認(rèn)不緩沖(或行緩沖)
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);
// 映射文件到內(nèi)存
char* map = (char*)mmap(nullptr, fileSize, PROT_WRITE, MAP_SHARED, fd, 0);
// 直接寫內(nèi)存,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系統(tǒng)調(diào)用可以一次寫多個不連續(xù)的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;
// 一次系統(tǒng)調(diào)用寫3個buffer
writev(fd, iov, 3);
這比3次write系統(tǒng)調(diào)用快,因為減少了用戶態(tài)/內(nèi)核態(tài)切換。
5. 跨語言通用優(yōu)化原則
| 原則 | Java | Python | C++ |
|---|---|---|---|
| 減少系統(tǒng)調(diào)用 | BufferedOutputStream | 批量write | writev/mmap |
| 避免內(nèi)存拷貝 | 直接ByteBuffer | memoryview | mmap/writev |
| 預(yù)分配大buffer | new byte[65536] | bytearray(65536) | vector<char> |
| 批量序列化 | ByteBuffer.put | struct.pack_into | memcpy |
| 異步刷盤 | NIO | 后臺線程 | msync(MS_ASYNC) |
6. 總結(jié)
三種語言的寫合并優(yōu)化思路相通,但實現(xiàn)不同:
| 語言 | 關(guān)鍵工具 | 核心優(yōu)化 |
|---|---|---|
| Java | BufferedOutputStream, ByteBuffer, MappedByteBuffer | 減少系統(tǒng)調(diào)用,避免JNI拷貝 |
| Python | bytearray, memoryview, struct.pack_into | 零拷貝視圖,直接buffer操作 |
| C++ | 手動buffer, mmap, writev | 穿透抽象,直接系統(tǒng)調(diào)用 |
關(guān)鍵認(rèn)知:
- 緩沖不是萬能的,大數(shù)據(jù)批量寫反而要避免二次緩沖
- 零拷貝(memoryview/mmap)比緩沖更快,但代碼更復(fù)雜
- 系統(tǒng)調(diào)用次數(shù)是瓶頸,每次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內(nèi)置函數(shù)memoryview()詳解. ?
-
CSDN博客. python內(nèi)置類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. ?