Como o sim do GNU é tão rápido?
$ yes | pv > /dev/null
... [10.2GiB/s] ...
Comparado com outros Unices, o GNU é incrivelmente rápido. NetBSD é 139MiB/s, FreeBSD, OpenBSD, DragonFlyBSD tem código muito semelhante ao NetBSD e provavelmente são idênticos, illumos é 141MiB/s sem argumento, 100MiB/s com. O OS X só usa a versão antiga do NetBSD semelhante ao OpenBSD, MINIX usa NetBSD, BusyBox é 107MiB/s, Ultrix (3.1) é 139MiB/s, COHERENT é 141MiB/s.
Vamos tentar recriar sua velocidade (não incluirei cabeçalhos aqui):
/* yes.c - iteration 1 */
void main() {
while(puts("y"));
}
$ gcc yes.c -o yes
$ ./yes | pv > /dev/null
... [141 MiB/s] ...
Não chega nem perto de 10,2 GiB/s, então vamos chamá-lo write
sem isso puts
a sobrecarga.
/* yes.c - iteration 2 */
void main() {
while(write(1, "yn", 2)); // 1 is stdout
}
$ gcc yes.c -o yes
$ ./yes | pv > /dev/null
... [6.21 MiB/s] ...
Espere um segundo, é mais lento do que puts
, Como isso é possível? Claramente, alguma prancheta acontece antes da escrita. Poderíamos vasculhar o código-fonte da glibc e descobrir, mas vamos ver Como as yes
faça isso primeiro.
Linha 80 dá uma dica:
/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item. */
Código abaixo que simplesmente copia o argv[1:] ou “y n” para a área de transferência, e assumindo que podem caber duas ou mais cópiascopia várias vezes para a área de transferência de BUFSIZ
. Então, vamos usar a área de transferência:
/* yes.c - iteration 3 */
#define LEN 2
#define TOTAL LEN * 1000
int main() {
char yes[LEN] = {'y', 'n'};
char *buf = malloc(TOTAL);
int used = 0;
while (used < TOTAL) {
memcpy(buf+used, yes, LEN);
used += LEN;
}
while(write(1, buf, TOTAL));
return 1;
}
$ gcc yes.c -o yes
$ ./yes | pv > /dev/null
... [4.81GiB/s] ...
Isso é muito melhor, mas por que não temos a mesma velocidade que os GNUs, sim? Estamos fazendo exatamente a mesma coisa, talvez seja algo a ver com isso full_write
função. Digging torna esta uma capa para uma capa para escrever (aproximadamente) apenas para escrever ().
Esta é a única parte do loop while, então talvez haja algo especial sobre o BUFSIZ deles?
eu cavei ao redor yes.c
‘s cabeçalhos para sempre, pensando que poderia ser uma parte config.h
que as ferramentas automáticas geram. Acontece que BUFSIZ é uma macro definida em stdio.h
:
#define BUFSIZ _IO_BUFSIZ
O que é isso _IO_BUFSIZ
? libio.h
:
#define _IO_BUFSIZ _G_BUFSIZ
Pelo menos o comentário sugere: _G_config.h
:
#define _G_BUFSIZ 8192
Agora tudo faz sentido, BUFSIZ é alinhado por páginas (as páginas de memória geralmente são 4096 bytes), então vamos alterar a área de transferência para caber:
/* yes.c - iteration 4 */
#define LEN 2
#define TOTAL 8192
int main() {
char yes[LEN] = {'y', 'n'};
char *buf = malloc(TOTAL);
int bufused = 0;
while (bufused < TOTAL) {
memcpy(buf+bufused, yes, LEN);
bufused += LEN;
}
while(write(1, buf, TOTAL));
return 1;
}
E, como sem usar os mesmos sinalizadores que yes
ele funciona mais lento no meu sistema (yes
no meu sistema é construído com CFLAGS="-O2 -pipe -march=native -mtune=native"
), faça diferente e atualize nosso benchmark:
$ gcc -O2 -pipe -march=native -mtune=native yes.c -o yes
$ ./yes | pv > /dev/null
... [10.2GiB/s] ...
$ yes | pv > /dev/null
... [10.2GiB/s] ...
Nós não vencemos os GNUs yes
, e provavelmente de jeito nenhum. Mesmo com o custo extra do recurso e as verificações extras de fronteira GNU yes
, a limitação não é o processador, mas a velocidade da memória. Com DDR3-1600 deve ser 11,97 GiB/s (12,8 GB/s), onde falta 1,5?
Podemos recuperá-lo com a montagem?
; yes.s - iteration 5, hacked together for demo
BITS 64
CPU X64
global _start
section .text
_start:
inc rdi ; stdout, will not change after syscall
mov rsi, y ; will not change after syscall
mov rdx, 8192 ; will not change after syscall
_loop:
mov rax, 1 ; sys_write
syscall
jmp _loop
y: times 4096 db "y", 0xA
$ nasm -f elf64 yes.s
$ ld yes.o -o yes
$ ./yes | pv > /dev/null
... [10.2GiB/s] ...
Parece que neste caso não podemos superar C ou GNU. A área de transferência é um segredo, e todos os custos incorridos devido ao kernel sufocam nosso acesso à memória, pipes, pv e redirecionamento são suficientes para negar 1,5 GiB / s.
O que aprendemos?
- Seu buffer de E/S para largura de banda mais rápida
- Navegue até os arquivos de origem para obter informações
- Você não pode superar seu hardware
Editar: _mrb conseguiu editar pv
alcance mais de 123GiB/s em seu sistema!!
Edit: menção especial a contribuição de agonnaz em vários idiomas!! Menção especial A implementação do Nekit1234007 dobra completamente a velocidade com vmsplice!!