是否使用缓冲区?fputc()与fwrite()性能对比及优化疑问
基准测试代码(ChatGPT生成)
#include <stdio.h> #include <stdlib.h> #include <time.h> #define SIZE 8388608 // #define SIZE 1073741824 int main() { FILE* file; char* buffer = malloc(SIZE); // Fill buffer with some data for (int i = 0; i < SIZE; i++) { buffer[i] = 'A'; } // Benchmark fwrite file = fopen("fwrite_test", "wb"); clock_t start = clock(); for (int i = 0; i < SIZE / BUFSIZ; i++) { fwrite(buffer + i * BUFSIZ, sizeof(char), SIZE / BUFSIZ, file); } clock_t end = clock(); fclose(file); printf("fwrite: %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC); // Benchmark fputc file = fopen("fputc_test", "wb"); start = clock(); for (int i = 0; i < SIZE; i++) { fputc(buffer[i], file); } end = clock(); fclose(file); printf("fputc : %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC); return 0; }
测试结果
8MiB文件测试结果
fwrite: 0.001915 seconds fputc : 0.092756 seconds
-> fwrite比fputc快48倍
1GiB文件测试结果
fwrite: 14.579728 seconds fputc : 3.512994 seconds
-> fputc比fwrite快4倍
实际待优化代码
#include "bitHandler.h" #include <stdint.h> #include <stdio.h> // Struct with all needed Infos to write buffered bits to a file typedef struct bitBuffer { unsigned char* buffer; unsigned int bufferPosition; int bitPosition; FILE* output; } bitBuffer_t; // UINT_FAST64_WIDTH is defined in c23, assuming 64 is reaasonable if compiler doesn't support c23 #ifndef UINT_FAST64_WIDTH #define UINT_FAST64_WIDTH 64 #endif // Write a single bit to buffer and then to file extern void writeBit(int bit, bitBuffer_t* bitBuffer) { if (bit) { bitBuffer->buffer[bitBuffer->bufferPosition] |= 1u << (7 - bitBuffer->bitPosition); } else { bitBuffer->buffer[bitBuffer->bufferPosition] &= ~(1u << (7 - bitBuffer->bitPosition)); } // Go to next byte if current one is full, flush buffer if it is full if (bitBuffer->bitPosition == 7) { if (bitBuffer->bufferPosition == BUFSIZ - 1) { flush(bitBuffer); } else { ++bitBuffer->bufferPosition; } bitBuffer->bitPosition = 0; } else { ++bitBuffer->bitPosition; } } // write multiple bits extern void writeBits(uint_fast64_t bits, int nbits, bitBuffer_t* bitBuffer) { int i; int bit; for (i = 0; i < nbits; i++) { bit = (bits >> (UINT_FAST64_WIDTH - 1 - i)) & 1u; writeBit(bit, bitBuffer); } } // write byte extern void writeByte(unsigned char byte, bitBuffer_t* bitBuffer) { int i; for (i = 0; i < 8; i++) { writeBit((byte >> (7 - i)) & 1u, bitBuffer); } } // flush the remaining bits in buffer to file extern void flush(bitBuffer_t* bitBuffer) { fwrite(bitBuffer->buffer, sizeof(char), (bitBuffer->bitPosition == 0) ? bitBuffer->bufferPosition : bitBuffer->bufferPosition + 1, bitBuffer->output); bitBuffer->bufferPosition = 0; bitBuffer->bitPosition = 0; }
问题解答
1. fputc()与fwrite()性能差异的原因
首先要明确:你的基准测试代码存在致命错误——fwrite循环里的第三个参数写错了,应该是BUFSIZ而不是SIZE / BUFSIZ。原代码中,每次fwrite都试图写入SIZE/BUFSIZ字节,总写入量变成了(SIZE/BUFSIZ)*(SIZE/BUFSIZ),远超出预期的SIZE,完全扭曲了测试结果。修正后,fwrite的性能会始终优于fputc,这才符合常规IO性能逻辑。
如果忽略这个错误假设结果真实,可能的解释是:
- 小文件场景:
fputc每次调用都要承担函数调用、单字节缓冲区管理的开销,而fwrite一次写入大块数据,函数调用次数极少,开销占比低,因此更快。 - 大文件场景:错误的
fwrite写法导致超量写入,引发磁盘IO过载;而fputc依赖C标准库默认缓冲区(通常也是BUFSIZ大小),实际底层IO次数和正确的fwrite相当,但错误的fwrite逻辑带来了额外的内存拷贝或磁盘压力,反而变慢。
但本质上,正确使用时,fwrite的性能不可能低于fputc——因为fputc内部也是基于缓冲区的,最终还是会调用类似fwrite的逻辑批量写入磁盘,额外多了一层单字节处理的函数调用和循环开销。
2. 追求IO最佳性能时,是否应该使用缓冲区
答案是必须使用缓冲区,但要注意正确的使用方式:
- C标准库本身已经为
FILE*对象提供了用户空间缓冲区(默认开启),fputc、fwrite都会先写入这个缓冲区,满了才会调用系统调用写入磁盘。 - 你自己实现的位缓冲区是必要的:哈夫曼编码是变长位流,无法直接用字节级的标准库缓冲区处理,必须先把位流组装成字节,再批量写入。
针对你的哈夫曼代码,优化方向包括:
- 重构
writeBits和writeByte,避免循环调用writeBit——直接操作位缓冲区,一次性写入多比特或整字节,减少函数调用和循环开销。 - 确保自定义缓冲区大小
BUFSIZ和标准库缓冲区大小匹配,或者设置为更大的数值(比如4KiB、16KiB,对应磁盘扇区或文件系统块大小),减少底层系统调用次数。 - 避免频繁调用
flush,只有在缓冲区满或者程序结束时才刷新,减少磁盘IO次数。
内容的提问来源于stack exchange,提问作者TheGlibber
相关产品推荐
相关产品推荐

