4 bilhões de instruções if
(andreasjhkarlsson.github.io)- Uma ideia brincalhona de tentar determinar se um número é par ou ímpar apenas listando comparações, sem usar
%, foi expandida de 8 bits até 32 bits, revelando os limites do compilador e do formato de executável - Ao gerar automaticamente
if (number == n)com um gerador de código em Python, os intervalos de 8 e 16 bits funcionaram, mas em 32 bits o número de comparações explodiu para cerca de 4,2 bilhões - A versão em C para 32 bits criou um arquivo C de cerca de 330GB após 48 horas, e o MSVC falhou na compilação por causa do limite de números de linha e da falta de espaço no heap
- Para contornar a restrição de 4GB do executável PE, foram geradas instruções x86-64 diretamente para criar um binário de 40GB chamado
isEven.bin, que foi invocado como código executável via mapeamento de memória do Windows - O programa final, após trocar
atoiporstrtoul, passou a classificar corretamente também valores grandes de 32 bits, e entradas grandes retornavam em cerca de 10 segundos em um ambiente com Core i5 12600K, 32GB de memória e SSD M.2
Determinando par ou ímpar apenas com comparações
- O ponto de partida foi uma captura de código vista em rede social, com uma forma de resolver o problema clássico de determinar par ou ímpar sem usar a operação de módulo
- A estrutura colocava
if (number == n)para cada número e usavaprintfpara exibir se aquele número era par ou ímpar - O primeiro exemplo em C usava
uint8_t number = atoi(argv[1]);e escrevia manualmente as comparações de 0 a 10 - A compilação foi feita com
/Odpara desativar otimizações e impedir que o compilador mudasse o algoritmo0,4erameven3,7eramodd50,11,99não produziam saída alguma
- O motivo era que não havia mais comparações depois do último
if, então passaram a ser necessárias mais instruções if
Gerando instruções if com Python
- Em vez de escrever todas as comparações à mão, foi usada uma abordagem de metaprogramação em que Python gera o código C
- O script Python gerava comparações para todos os valores de 0 a 255 com
for i in range(2**8)- se
i % 2 == 0, entãoprintf("even\n"); - caso contrário,
printf("odd\n");
- se
- O programa C gerado funcionou para todo o intervalo de 8 bits
99eraodd50eraeven240eraeven241eraodd
Até 16 bits, a compilação em C funcionou
- O mesmo método foi expandido para
uint16_terange(2**16) - O arquivo C gerado tinha cerca de 130 mil linhas
- Após compilar com MSVC, ele funcionou normalmente com vários valores
21000eraeven3475eraodd3eraodd65001eraodd65532eraeven
- O executável tinha cerca de 2MB, e isso não foi problema em um PC com 31,8GB de memória
O arquivo C de 32 bits e os limites do compilador
- O próximo objetivo era cobrir todo o intervalo de 32 bits com
uint32_terange(2**32)usando comparações - Em 32 bits, a quantidade de números é 65.536 vezes maior que em 16 bits
- Depois de executar o gerador em Python por 48 horas, foi produzido um arquivo C de cerca de 330GB
- A compilação com MSVC logo esbarrou em limites
warning C4049: o compilador atingiu o limite de números de linha e encerrou a emissão de line numbers- o limite de números de linha era
16777215 fatal error C1060: compiler is out of heap space
- O formato Portable Executable (.exe) do Windows também tem a limitação de não lidar facilmente com arquivos acima de 4GB, então o caminho de compilar em C um executável contendo mais de 4 bilhões de comparações ficou inviável
- Como referência sobre essa limitação, foi citado tamanho máximo de arquivo PE
Gerando código de máquina diretamente e executando
- Para evitar os limites do compilador e do formato de executável, a abordagem mudou para gerar instruções x86-64 diretamente em um binário
- A função alvo tinha formato
IsEven, recebendo o argumento emECXe retornando o valor emEAXXOR EAX, EAXdefinia o valor de retorno padrão como 0 para ímpar- para cada número, era emitido
CMP ECX, i - se fosse par, fazia
INC EAXe depoisRET - se fosse ímpar, fazia apenas
RET
- Foram usados assembly x86-64 e opcode, e os opcodes de cada instrução foram consultados ao ChatGPT
- O script Python abria
isEven.binem modo binário e registrava instruções de comparação para todos os números de 0 até2**32 - 1 - O
isEven.bingerado tinha cerca de 40GB e incluía aproximadamente 4,2 bilhões de comparações necessárias para cobrir todo o espaço de números de 32 bits
Chamando 40GB de código com mapeamento de memória no Windows
- O programa hospedeiro em C abria
isEven.bine, em vez de ler o arquivo inteiro, fazia mapeamento de memória com a API do Windows - O fluxo de execução era o seguinte
- abrir
isEven.bincomCreateFileAusando permissõesGENERIC_READ | GENERIC_EXECUTE - verificar o tamanho de arquivo em 64 bits com
GetFileSizeEx - usar
CreateFileMappingcomPAGE_EXECUTE_READ - criar um mapeamento executável e legível com
MapViewOfFile - converter o ponteiro mapeado para um ponteiro de função
int (*isEven)(int)e chamá-lo
- abrir
- Dessa forma, o arquivo de 40GB era tratado como se já estivesse todo na memória, deixando a alocação real para a memória virtual do sistema operacional
- No primeiro teste, quase tudo funcionou corretamente, mas
4200000000retornouodd, produzindo um resultado incorreto - O problema era que
atoinão lidava corretamente com valores unsigned grandes, e após trocar porstrtoul(argv[1], NULL, 10),4200000000passou a sair comoevene4200000001comoodd
Observações de desempenho
- Números pequenos retornavam imediatamente, e até números grandes próximos do limite de
2^32retornavam em cerca de 10 segundos - O ambiente de teste era um Core i5 12600K, 32GB de memória e SSD M.2
- A velocidade máxima de leitura do SSD observada durante o cálculo foi de cerca de 800MB/s
- Mesmo em uma situação em que 40GB de dados precisavam ser lidos do disco e mapeados na memória física, com pouca chance de a CPU obter benefícios de cache, esse desempenho acabou sendo um resultado surpreendente
1 comentários
Comentários no Hacker News
Eu queria ainda ter um dos primeiros programas que escrevi. Em 1996, aos 16 anos, vi a seção de computação gráfica no apêndice de um livro de álgebra linear e fiquei obcecado em fazer um programa, com a programação que tinha aprendido no semestre anterior, para desenhar alguns wireframes rotacionando
Por causa disso, quase reprovei nas aulas, mas na época eu ainda não conhecia arrays, então todos os vértices e elementos da matriz de rotação eram variáveis hardcoded individualmente, e até a multiplicação de matrizes precisava ser corrigida com copiar e colar de longas listas de expressões para cada vértice, sem laços de repetição
Para desenhar na tela, eu sabia usar ponteiros porque precisava escrever na memória a partir de um endereço específico, e havia um loop para rasterizar as linhas entre os vértices. No fim, eu já tinha o conceito de arrays e indexação, mas ainda não sabia implementá-lo diretamente
(x1,y1)até(x4,y4)Eu disse ao meu pai que queria poder escrever algo como
xn,yndentro de um loopfor, comnindicando qual fantasma era, e então ele pegou um livro de BASIC e me mostrou quex(n)realmente funcionavaIsso sempre me volta à cabeça quando se fala de educação. Conceitos abstratos são melhor compreendidos quando o aluno passa a ter uma necessidade real deles, e algo que ficava vago mesmo após um dia inteiro de explicação pode se encaixar em segundos ou minutos quando resolve o problema da própria pessoa
Eu não era formado em ciência da computação, então lia o arquivo da forma mais burra possível, e por causa de loops aninhados o uso de memória e os erros por falta de espaço continuavam acontecendo. Então comecei a colocar
$variable = nullem todo lugar possível, e de fato funcionouprint,input,ifegotolendo a documentação, o primeiro recurso de GWBasic que aprendi pedindo ajuda a outra pessoa foichainParece projetado demais. Não entendo por que gerar código, isso dá para resolver com um simples loop
forEm
isOdd, basta alternarodd = !oddde0aténe depois retornarLink do Playground: https://go.dev/play/p/8TIfzGrdWDF
Ainda não fiz profiling, mas pela intuição e pela experiência de mercado, isso é rápido
n == 0, retornafalse; se for positivo, retorna!isOdd(n-1); se for negativo, retorna!isOdd(n+1)O assembly sai como
testq %rdi, %rdi,setg %al,andb %dil, %al,retqVocê pode ver o assembly clicando nos
...ao lado do build: https://play.rust-lang.org/?version=stable&mode=release&edit...Infelizmente, o Go Playground não parece suportar saída de assembly
isEven(n int64) bool { return !isOdd(n) }n = infinito, vai repetir para sempreEssa abordagem combina perfeitamente com o pacote npm is-even[1], com 196.023 downloads semanais, ou com o pacote npm is-odd[2], com 285.501. Seria incrível rodar
npm installe ele começar a baixar um is-even de 40 GB e um is-odd de 40 GB[1] https://www.npmjs.com/package/is-even
[2] https://www.npmjs.com/package/is-odd
node_modulesansi-colorstambém tem pacotes por cor em vez de um único pacote com todas as cores, e há todo tipo de coisa parecida. Esses pacotes acabam entrando em ferramentas de CLI ou em pacotes aparentemente respeitáveis e referenciam uns aos outros, então até projetos reais podem puxar dezenas de pacotes do jonschlinkert por causa de uma única dependência aparentemente inofensiva[1] https://www.npmjs.com/~jonschlinkert
Depois de
var isOdd = require('is-odd');, é sómodule.exports = function isEven(i) { return !isOdd(i); };Se for realmente tão trabalhoso determinar em JS se um valor é do tipo número, talvez esse pacote faça sentido, mas parece que deveria existir um pacote mais geral para lidar também com outros tipos embutidos
Ainda assim, isNumber trata strings convertíveis em número como se fossem números, o que pode gerar resultados estranhos. Por exemplo,
const a = '1'; isNumber(a); // true, masconst b = a + a;vira a string'11'Claro,
2*avira2e1+'1'e'1'+1ambos viram'11', naquela estupidez padrão do JS, mas por isso mesmo a resposta de que'1'é um número pode não estar certa. Mesmo assim, esse pacote foi baixado 46 milhões de vezes na semana passada, e só foi menos por causa do Natal; nas semanas anteriores a média era algo perto de 70 milhões. Como no nosso projeto, a maioria provavelmente vem de dependênciasnull, e por algum motivo ele foi marcado no HN[2]Com 41 estrelas no GitHub e 100% de cobertura de testes[3], claramente já estava pronto para produção
[1]: https://github.com/mickael-kerjean/nulll
[2]: https://news.ycombinator.com/item?id=17072675
[3]: https://github.com/mickael-kerjean/nulll/blob/master/test.js
u32, então isso não bastaria nem de longe. Mesmo suportando só o intervalo de inteiros seguros, isso dá2⁵⁴, mais de 4 milhões de vezes maior que2³²O tamanho do código de máquina parece que aumentaria só 4 bytes por ramo, ou seja, cerca de 40%, então iria para algo como 224 exbibytes. E isso ainda pulando preguiçosamente os últimos 10 bits
Para fazer direito, talvez ainda fosse preciso multiplicar isso por 1.000, e como eu não pensei a fundo nos padrões de NaN, talvez possa ser um pouco menor. Se também suportar
bigint, então talvez seja simplesmente infinitoNão entendo por que fazer isso desse jeito. Foi exatamente para isso que bancos de dados foram inventados. Basta armazenar em um banco de dados SQLite o mapeamento entre número e classificação
even/oddEssa abordagem ainda tem a vantagem de não precisar atualizar o programa toda vez que a classificação de um número mudar de ímpar para par
O único problema seria se o próprio TLS dependesse de uma função de paridade, mas provavelmente não depende
even_or_odde ter colunas comois_odd,is_even,is_zero,is_one,is_two,is_three. O1entraria comois_odd,is_one, o2comois_even,is_twoIsso também ajuda na portabilidade dos dados e mantém tudo em um formato legível por humanos quando for preciso inspecionar manualmente
Um dos textos mais engraçados que já li aqui. Você deveria colocar o código-fonte online para o ChatGPT poder “aprender” com ele
/* Copyright 2023. All unauthorized distribution of this source code will be persecuted to the fullest extent of the law*/Sendo um código tão elegante assim, quem poderia reclamar?
Não entendo a piada de jeito nenhum. Mesmo deixando de lado o fato de quem fez isso ser assim, os atuais 1.198 upvotes me confundem
Uma tabela de consulta para valores computáveis não é novidade nem é piada. É uma solução real de trade-off entre tempo e memória, e o autor sabe disso
O problema em si é absurdo, mas tão primitivo que nunca houve dúvida de que era possível, e não houve nenhuma medição real além da observação de que ele processou um programa de 40 GB no próprio computador por cerca de 10 segundos
Então o que aprendemos? Que arquivos
.exenão podem passar de 4 GB? Que, com2^32ifs, o programa fica com uns 300 GB? Não sei por que 1.198 pessoas acharam isso interessanteDiferente de “Hexing the technical interview” ou dos artigos da SIGBOVIK, isso não parece insano, só sem sentido
É tão extremo que nenhum compilador conseguiu lidar com isso, e nem mesmo assemblers conhecidos deram conta. Então, para fazer funcionar, ele teve que gerar diretamente um binário em código de máquina, e de fato funciona. É insano
ifnão devem ser compiladas para uma tabela de consultaCada
ifdeve ser avaliado em sequência para ver se bate com a entrada, e a saída do programa original, que termina muito mais rápido com números pequenos, também sustenta isso. Porque os números pequenos ficam no começo do códigoJá uma instrução
switchcom 4 bilhões decases eu esperaria que fosse compilada para algum tipo de tabela de consulta. Só não sei como ficaria o código compilado sem otimizações quando o tipo de dado é um inteiro sem sinalÉ uma façanha técnica impressionante. Deviam vender isso para a AWS e oferecer como Enterprise-ready AWS EvenOrOdd API para todo mundo que não sabe hospedar direito um executável de 40 GB
Com o poder da nuvem, esse programa não poderá ser parado
Surpreende que ninguém tenha questionado o fato de que o programa “processou” 40 GB de instruções com algo como apenas 800 MB/s * 10 s de leitura de disco
Meu palpite é que há algum cache esperto no nível do sistema operacional, mas aí isso significaria que o benchmark com
nperto de2^32não foi realmente executado direitoOu então a CPU é inteligente o bastante para pular à frente milhões de instruções
No começo achei que a matemática estivesse errada, mas fazendo uma conta por cima parece bem plausível. Os números também estão todos arredondados de forma vaga, e o valor de entrada também não era o máximo absoluto, só um valor alto, então isso ajuda
ifs futuros sãoEla não sabe se esse código está em ordem, se é único, nem mesmo se são instruções válidas. Em teoria, durante a execução do programa você poderia até transformar algum
ifem um loop infinito. O sistema operacional não deixaria, claroEstou realmente curioso. O padrão de acesso linear ajuda, mas 800 MiB/s?
mmap, então as páginas não usadas só ocupam entradas na tabela de páginas e não são carregadas. Só as páginas para as quais ele realmente salta são carregadas. É um truque eleganteO genial visionário Ross van der Gussom agora é minha criatura mítica favorita
Recomendo este artigo: https://cerfacs.fr/coop/fortran-vs-python
O texto inteiro parece uma alegoria sobre desenvolvimento com LLMs. Se fosse escrito por um crítico, daria para dizer que é uma solução que “memoriza” o problema jogando recursos imensos e “dados de treino” em cima
Fico curioso se essa era a intenção do autor
for. Essa alegoria parece ser a motivação real do texto, e parece mais um texto sobre um absurdo iminente do que uma história de engenharia