Como o GNU “sim” é tão rápido? : unix

By | Junho 4, 2022

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!!

Deixe uma resposta

O seu endereço de email não será publicado.