- I/O com buffer agrupa dados em buffers no espaço do usuário (ou da biblioteca) e realiza menos chamadas de sistema, porém maiores. Ótimo para throughput.
- I/O sem buffer envia dados diretamente (ou de forma mais direta) ao kernel/dispositivo, oferecendo menor latência e timing mais previsível — a um custo de throughput para muitas operações pequenas.
- Use I/O com buffer para processamento normal de arquivos e tarefas de alto throughput. Use I/O sem buffer ou síncrono quando precisar de visibilidade imediata, determinismo ou durabilidade estrita.
O que ‘com buffer’ e ‘sem buffer’ significam
- I/O com buffer: o runtime (por exemplo, o
FILE*da biblioteca padrão do C) mantém um buffer intermediário e só chamaread()/write()quando o buffer está cheio, vazio ou é esvaziado. Isso reduz o número de chamadas de sistema e geralmente melhora o throughput. - I/O sem buffer: suas operações são passadas ao kernel (ou dispositivo) com o mínimo de agregação no espaço do usuário. Exemplos: chamar
read()/write()do POSIX repetidamente com tamanhos pequenos.
Duas camadas de buffer no Unix
- Buffer no espaço do usuário (stdio, runtime da linguagem) — o que a maioria das pessoas quer dizer com “I/O com buffer” no nível da aplicação.
- Page cache do kernel — mesmo depois que
write()retorna, o kernel frequentemente mantém o conteúdo na RAM (o page cache) até gravá-lo no disco. Para tornar os dados duráveis em armazenamento estável, você precisa defsync()/fdatasync()ou das semânticasO_SYNC/O_DSYNC.
Entender as duas camadas importa: você pode fazer fflush() (espaço do usuário) e ainda precisar de fsync() (kernel/disco) se exigir persistência.
Tradeoffs de desempenho e correção
-
I/O com buffer
- Prós: menos chamadas de sistema, maior throughput para muitas operações pequenas, saída formatada conveniente.
- Contras: os dados podem ficar nos buffers do espaço do usuário; se o seu processo travar antes do flush, esses dados são perdidos.
-
I/O sem buffer / síncrono
- Prós: visibilidade de saída com menor latência e timing mais previsível; pode ser necessário para usos de dispositivo ou tempo real.
- Contras: mais chamadas de sistema, geralmente menor throughput para muitas escritas pequenas, e escritas síncronas no disco podem ser lentas.
Quando usar I/O com buffer
- Processamento normal de arquivos (ler/escrever arquivos, copiar dados).
- Cargas de trabalho que fazem muitas escritas pequenas — deixe um buffer combiná-las em escritas maiores no kernel.
- Log típico de aplicação, onde você aceita o flush eventual para o disco (ou depende de rotação de logs / coletor externo).
Use os padrões do runtime na maioria dos casos — eles são ajustados para cargas comuns.
Quando usar I/O sem buffer ou síncrono
- Saída interativa que precisa ser visível imediatamente (por exemplo, REPLs, barras de progresso). Para stdio, use line-buffering ou chame
fflush(stdout)após mensagens importantes. - Logs críticos que precisam estar no disco antes de continuar (use
fsync()ou abra comO_SYNC, entendendo o custo de desempenho). - Dispositivos/bancos de dados que gerenciam seu próprio cache e exigem controle direto.
- Sistemas de tempo real ou baixa latência, onde um timing previsível importa.
Exemplos em C: com buffer vs sem buffer
Abaixo estão dois programas mínimos em C que demonstram a diferença. O primeiro usa a biblioteca padrão do C (stdio com buffer). O segundo usa open()/write() do POSIX diretamente (sem buffer do ponto de vista do espaço do usuário).
Com buffer (stdio):
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
/* cast to void to silence compiler warnings */
(void)argc;
(void)argv;
FILE *f = fopen("buffered.out", "w");
if (!f) {
fprintf(stderr, "Unable to open: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
for (int i = 0; i < 1000000; i++) {
/* goes into stdio buffer */
fprintf(f, "line %d\n", i);
}
/* flush user-space stdio buffer into the kernel */
fflush(f);
/* optionally ensure the kernel pushes to stable storage */
fsync(fileno(f));
fclose(f);
return EXIT_SUCCESS;
}
Sem buffer (write do POSIX):
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
/* cast to void to silence compiler warnings */
(void)argc;
(void)argv;
int file_descriptor = open("unbuffered.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (file_descriptor < 0) {
fprintf(stderr, "Unable to open: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
for (int i = 0; i < 1000000; i++) {
char buf[64];
int n = snprintf(buf, sizeof buf, "line %d\n", i);
/* each write() is a separate system call
* and bypasses stdio buffering
*/
if (write(file_descriptor, buf, n) != n) {
fprintf(stderr, "Unable to write: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
}
/* ensure kernel writes to stable storage */
fsync(file_descriptor);
close(file_descriptor);
return EXIT_SUCCESS;
}
Compilação/execução rápida:
$ cc --version
cc (nb3 20231008) 10.5.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ cc -Wall -Werror -Wextra buffered.c -o buffered
$ cc -Wall -Werror -Wextra unbuffered.c -o unbuffered
$ time ./buffered
0.18 real 0.00 user 0.13 sys
$ time ./unbuffered
1.26 real 0.15 user 0.94 sys
Compare os tempos e observe que a versão com buffer normalmente realiza menos chamadas de sistema e geralmente será mais rápida para muitas escritas pequenas.
Guia rápido de decisão
- Throughput para muitas operações pequenas → I/O com buffer (padrão). Veja o exemplo anterior.
- Precisa ver a saída imediatamente ou garantir ordenação/durabilidade → flush + fsync ou use I/O síncrono/sem buffer.
- Cache gerenciado pelo dispositivo ou pelo banco de dados → siga a recomendação do dispositivo/biblioteca (muitas vezes I/O sem buffer/direto).
Reflexão final
O I/O com buffer é o padrão sensato: oferece melhor desempenho com baixa complexidade. Use I/O sem buffer ou síncrono de forma deliberada quando precisar de suas propriedades — determinismo, visibilidade imediata ou forte durabilidade — e meça o custo. Na dúvida, prefira as APIs de alto nível padrão e adicione flush ou sync explícito onde a correção exigir.