C++/Java/Python寫合并優(yōu)化實戰(zhàn)

上一篇我們聊了寫合并,實際情況中寫文件、發(fā)網(wǎng)絡(luò)包、填顯存,如果逐字節(jié)/逐字段操作,性能會慘不忍睹。這一篇具體講講在業(yè)務(wù)代碼中可能遇到的情況。


1. 核心思想:緩沖+批量

無論哪種語言,寫合并的核心都是兩個手段:

  1. 用戶態(tài)緩沖:在內(nèi)存里攢數(shù)據(jù),滿了再刷
  2. 批量操作:一次系統(tǒng)調(diào)用傳輸大塊數(shù)據(jù)

代價是延遲增加(數(shù)據(jù)在緩沖區(qū)等),但吞吐量提升10-100倍。


2. Java:BufferedOutputStream不是銀彈

Java的IO流設(shè)計體現(xiàn)了寫合并思想,但用不好反而慢。

2.1 BufferedOutputStream原理

BufferedOutputStreamOutputStream上加了個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])

memoryview的關(guān)鍵特性[6][7]

  • 不拷貝數(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的問題:

  1. 虛函數(shù)開銷(多態(tài))
  2. 每次操作都檢查流狀態(tài)
  3. 默認(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)知

  1. 緩沖不是萬能的,大數(shù)據(jù)批量寫反而要避免二次緩沖
  2. 零拷貝(memoryview/mmap)比緩沖更快,但代碼更復(fù)雜
  3. 系統(tǒng)調(diào)用次數(shù)是瓶頸,每次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內(nèi)置函數(shù)memoryview()詳解. ?

  5. CSDN博客. python內(nèi)置類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. ?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容