2 pontos por GN⁺ 2024-08-05 | 1 comentários | Compartilhar no WhatsApp
  • Em código criptográfico, a propriedade crucial de tempo constante (constant-time) pode ser quebrada apenas por otimizações do compilador, e um experimento está em andamento para inserir um patch de aviso dentro do LLVM para encontrar padrões de risco
  • A “otimização” do compilador pode acelerar parte dos benchmarks, mas muitas vezes os caminhos realmente críticos dependem de intrinsics e assembly, e o custo dos bugs gerados pela otimização se acumula separadamente
  • Em junho de 2024, Antoon Purnal confirmou que o código de referência do Kyber, com Clang 15 ou superior e certas opções de otimização, pode ser transformado em desvios condicionais baseados em valores secretos, permitindo ataques de timing
  • O TIMECOP 2 inspeciona, dentro do SUPERCOP, os resultados compilados declarados como constantes no tempo, mas há limitações tanto nos comandos suportados pelo Valgrind quanto no fluxo de dados exposto durante a execução real dos testes
  • Na prática, a resposta é usar funções como crypto_{int,uint}{8,16,32,64}.h para impedir que o compilador trate resultados de 1 bit como bool, ou migrar para assembly verificado, linguagens voltadas à segurança ou compiladores dedicados

O vazio de responsabilidade criado pela “otimização” do compilador

  • Nos históricos recentes de mudanças de LLVM e GCC, continuam aparecendo “otimização”, testes de “otimização”, correções de testes e correções de bugs de “otimização”
  • Mesmo quando um código funcionava bem antes da compilação e muda depois de alterações no compilador, em muitos casos a responsabilidade recai sobre o programador por ter esbarrado em “undefined behavior”
  • Esses “language standards” são definidos por autores de compiladores e, no fim, formam uma estrutura em que o código de milhões de programadores assume mais responsabilidade do que as mudanças feitas por um pequeno grupo de autores de compiladores
  • Como exemplo em código criptográfico, em vários benchmarks de CPU a implementação avx2 de kyber768 é cerca de 4 vezes mais rápida do que o código portátil compilado com compiladores “otimizadores”

Limites da medição de desempenho da otimização

  • Em 2000, Todd A. Proebsting expressou em Proebsting's Law que “o avanço dos compiladores dobra o poder computacional a cada 18 anos”, concluindo que a contribuição da otimização de compiladores é periférica
  • Em 2022, Arseny Kapoulkine resumiu em um benchmark que o LLVM 11 leva o dobro do tempo do LLVM 2.7 para compilar com otimização, enquanto o código executável fica em geral 10 a 20% mais rápido
  • As duas discussões deixam passar a medição de desempenho realmente percebida pelos usuários
    • Os hotspots onde o desempenho se concentra costumam conter muitos intrinsics e assembly
    • O FFmpeg tem 160.000 linhas de assembly em arquivos .asm e .S
    • À medida que computadores e redes processam mais dados, mais tempo real de CPU recai sobre esses hotspots
  • O custo de segurança também cresce separadamente nas discussões sobre otimização
    • A Deloitte informou que, em 2023, o orçamento de segurança de TI foi 0,5% da receita das empresas
    • Junto com o dado de que a receita total das empresas no mundo em 2022 foi de mais de 48 trilhões de dólares, a escala total pode chegar à casa das centenas de bilhões de dólares
    • Ainda assim, há a ressalva de que os 0,5% da Deloitte podem ser uma média simples por empresa, e nem todas as empresas responderam à pesquisa

Vazamento por timing e o caso Kyber

  • Os problemas de segurança criados por compiladores “otimizadores” incluem não só bugs tradicionais, mas também vazamentos por timing em que informação secreta escapa pelo tempo de execução
  • O artigo da EuroS&P 2018, de Laurent Simon, David Chisnall e Ross Anderson, alertou que uma atualização de compilador pode abrir silenciosamente canais de timing em código que antes era seguro
  • O exemplo destacado no artigo de 2018 era um código que escolhia entre dois valores com bool, e esse bool induzia o compilador a gerar saltos condicionais
    • Em implementações criptográficas, é prática comum evitar isso removendo bool de trechos importantes e criando funções separadas de comparação em tempo constante
    • O OpenSSL é citado como declarando 37 funções para isso
  • O caso de 2015 envolvendo curve25519-donna e MSVC 2015 é tratado no texto como um mal-entendido
    • Na prática, ao compilar para x86 de 32 bits, operações int64 eram convertidas em chamadas para a biblioteca int64 de 32 bits da Microsoft, llmul.asm
    • O vazamento por timing era criado por desvios dependentes de dados em llmul.asm, e essa biblioteca também deve ser considerada parte de um conceito razoável de código-fonte
  • Em junho de 2024, Antoon Purnal confirmou que o código de referência do Kyber pode permitir ataques de timing com Clang 15 ou superior e certas opções de otimização
    • A forma problemática era (-((x>>j)&1))&y, um cálculo que produz y se o bit j de x estiver definido, ou 0 caso contrário
    • O Clang transforma o bit em bool usando uma instrução de teste de bit e então gera um desvio condicional com base nesse bool
    • Dentro do LLVM, essa “otimização” é tratada por combineShiftAnd1ToBitTest em lib/CodeGen/SelectionDAG/DAGCombiner.cpp
    • Essa função foi adicionada por Sanjay Patel em setembro de 2019 e depois modificada por várias pessoas
  • Há casos parecidos de invasão de limites também no GCC
    • Um patch do GCC da ARM, de novembro de 2021, transforma (-x)>>31 em -(x>0)
    • Em abril de 2024, surgiu um alerta a respeito disso

TIMECOP e verificação de tempo constante

  • O TIMECOP 2 está embutido no framework de testes criptográficos SUPERCOP e verifica automaticamente, em código compilado declarado como constante no tempo, desvios condicionais derivados de valores secretos
  • O escopo da verificação inclui, além de desvios condicionais, índices de array derivados de valores secretos
    • O artigo KyberSlash também descreve um patch para verificar divisões derivadas de valores secretos
  • O TIMECOP 1 foi uma ferramenta criada por Moritz Neikes ao modificar o SUPERCOP, automatizando a abordagem ctgrind de Adam Langley
  • O TIMECOP 2 amplia a abordagem anterior em alguns pontos
    • Marca automaticamente saídas do RNG como valores secretos
    • Dá suporte a “declassification”
    • Dá suporte à especificação de “public inputs”
    • É executado em vários núcleos
  • O TIMECOP tem limitações claras
    • Só consegue lidar com instruções suportadas pelo Valgrind, então para em casos como instruções AMD XOP
    • Inspeciona apenas o fluxo de dados visível durante a execução real dos testes
  • O trabalho com ferramentas de verificação de comportamento em tempo constante continua, e uma lista de ferramentas relacionadas está em ct-tools
  • Uma verificação correspondente à do TIMECOP entrou na suíte de testes do libmceliece e pode se espalhar para outras bibliotecas

Abordagem de reescrita para tempo constante

  • Depois de encontrar um trecho de código com tempo variável, é preciso uma forma de reescrevê-lo em tempo constante sem introduzir bugs
  • Em uma apresentação de julho de 2024, foram mostradas algumas das funções de tempo constante oferecidas por libmceliece e SUPERCOP
    • Os nomes de arquivo são crypto_{int,uint}{8,16,32,64}.h
    • Esses arquivos podem ser copiados para uso em outros projetos
  • A função de exemplo crypto_uint32_bitmod_mask(x,j) produz o mesmo efeito de -((x>>(j&31))&1), mas impede que o compilador enxergue o resultado de 1 bit
  • Há também exemplos mais complexos, como crypto_uint32_max(x,y)
  • O artigo de 2018 trata de um tweak para adicionar ao Clang/LLVM a função de tempo constante __builtin_ct_choose(bool cond, x, y)
    • O artigo sugeriu, incorretamente, que essa única função seria suficiente
    • Essa função pode até entrar no compilador algum dia, mas pode levar muito tempo até que projetos possam depender dela
    • A implementação parece mais frágil do que crypto_{int,uint}{8,16,32,64}.h

Como evitar o problema com antecedência

  • Se testes antes da distribuição de uma biblioteca compilada detectarem vazamentos por timing introduzidos pelo compilador, a distribuição pode continuar usando a versão anterior do compilador enquanto o código é reescrito
    • Essa abordagem é uma resposta temporária para continuar mantendo os usuários seguros
  • Uma solução é distribuir a biblioteca em assembly
    • A apresentação da RWC 2024 Adoption of high-assurance and highly performant cryptographic algorithms at AWS mostra um software rápido de X25519 cuja correção para todas as entradas foi provada
    • A implementação foi escrita em assembly em 2 versões para CPUs Intel/AMD de 64 bits e 2 versões para CPUs ARM de 64 bits
    • A proposição de correção é um teorema sobre o código de máquina realmente executado pelos usuários, e a prova foi verificada com o provador de teoremas HOL Light
  • Ainda assim, em software criptográfico que não chega a esse nível, continua existindo o problema da dificuldade de auditoria de assembly
  • Também se busca uma forma rápida de aplicar uma “vacina” contra vazamentos por timing em código escrito em C, C++ e afins

Experimento com o patch clang-vs-clang

  • x&1 e x>>31 têm em comum o fato de que só admitem dois resultados possíveis
    • x&1 é 0 ou 1
    • x>>31 de uint32 é 0 ou 1
    • x>>31 de int32 é 0 ou -1
  • Essas formas facilitam que quem escreve “otimizações” de compilador coloque o resultado de 1 bit em bool
  • Há a recomendação de sempre compilar com -fwrapv para que GCC e Clang assumam aritmética em complemento de dois
  • Em vez de apenas varrer o código-fonte atrás de &1, 1&, >>31 etc., foi inserido diretamente um patch no “optimizer” do LLVM para fazer a varredura de outro modo
  • O patch parte do commit 68df06a0b2998765cb0a41353fcf0919bbf57ddb do LLVM, encontra &1 e >>31 e emite o seguinte aviso
    • please take this away before clang does something bad
  • Um comando de compilação de exemplo é clang -Rpass-analysis=clang-vs-clang -O -c x.c
  • A função de teste é a seguinte
int sra31(int x)
    {
      x >>= 31;
      return x;
    }
  • Não é surpreendente que o mesmo aviso se repita
    • O compilador continua tentando aplicar “otimizações” até que não haja mais progresso
  • A saída de clang-vs-clang distingue signed de unsigned em shifts
    • Essa diferença é importante para reescritas manuais ou automáticas com base em crypto_{int,uint}{8,16,32,64}.h
    • Um dos métodos para automatizar a transformação do código-fonte é o clang-tidy
  • Códigos removidos por #ifdef ou eliminados antes dessa etapa de “otimização” não geram avisos do clang-vs-clang

Resultados de execução do SUPERCOP e casos encontrados

  • O SUPERCOP 20240716 foi executado em um dual EPYC 7742 com ./data-do-biglittle
    • O overclocking foi desativado
    • A lista de compiladores do SUPERCOP foi ajustada para usar clang-vs-clang, adicionando -Rpass-analysis=clang-vs-clang à linha clang em okcompilers/{c,cpp}
  • Os resultados ficaram prontos depois de 3 horas
    • A saída do Clang teve 675.752 linhas no total
    • O tamanho bruto foi de 210.786.494 bytes
    • O resultado comprimido foi 20240803-fromclang.txt.gz, com 3.595.199 bytes
  • A saída contém muito ruído vindo de ramificações de código-fonte baseadas em dados públicos que acabam gerando &1 dentro do Clang
  • Um exemplo claro para alteração preventiva é o seguinte
a0 += (a0>>15)&106;
  • Um exemplo que exigiria esforço de parsing de C para ser encontrado por simples varredura de código-fonte é o seguinte
    • A macro ONE8 é definida como ((uint8_t)1)
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
  • Exemplos ainda mais difíceis aparecem em macros baseadas em intrinsics AVX2
    • signmask_x16(x) é definido como _mm256_srai_epi16((x),15)
    • Isso desloca cada pedaço signed de 16 bits dentro de um vetor de 256 bits em 15 bits para a direita
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
  • Esse caso com AVX2 não é prioridade alta
    • Para que operações vetoriais virem desvios condicionais, seria preciso compilar com AVX-512 e o compilador ainda tomar a decisão estranha de transformar bool vetorizado em desvios condicionais bool seriais
    • O TIMECOP usa Valgrind, e o Valgrind não suporta AVX-512
    • Por enquanto, não se recomenda compilação com AVX-512

int128 e uma direção de resposta mais ampla

  • A descoberta mais interessante foi um caso em que shift à direita de 64 bits em int128 gerou um aviso de >>
  • A implementação de int128 pode internamente usar um shift à direita de 63 bits para descobrir o sinal da palavra superior de 64 bits
  • Se o Clang adicionar, como o GCC, suporte para transformar shift à direita de 63 bits em bool e depois em desvio condicional, muito código com int128 pode de repente se tornar de tempo variável
    • Nesse caso, a situação se aproximaria da que o título do artigo de 2015 afirmava, mas desta vez ocorreria mesmo sem bool explícito no código-fonte
  • No nível do código-fonte, a proteção mais simples é evitar a implementação atual de int128 do compilador e usar funções crypto_int128
    • crypto_int128, ao contrário de int128 do GCC e do Clang, também pode funcionar em plataformas pequenas de 32 bits
  • A ideia de adicionar tipos de dados secretos ao GCC e ao Clang parece boa, mas não está claro como torná-la robusta na estrutura dos dois compiladores
  • Há mais expectativa em compiladores projetados desde o início para segurança
    • Entre os compiladores voltados à segurança que exigem uma nova linguagem de entrada estão FaCT e Jasmin, em desenvolvimento ativo
    • Há preocupação com o tempo de reescrita do código, mas, vendo como os compiladores atuais tratam o código existente, algum tipo de ação será necessário

1 comentários

 
GN⁺ 2024-08-05
Opiniões no Hacker News
  • Não faz sentido chamar de bug do compilador quando um código que contém comportamento indefinido não se comporta como desejado.
    É parecido com executar dd com argumentos errados, perder os dados e depois dizer que o dd tem bug.

    • Parece que o autor confundiu aqui comportamento definido pela implementação com comportamento indefinido. Os exemplos do texto são, em sua maioria, código válido, e o problema real é que uma otimização do compilador que transforma aritmética de operações bit a bit em desvios torna possíveis ataques de temporização em código criptográfico.
      É difícil dizer que o código-fonte ou o compilador têm bug; o mais correto é ver isso como o padrão C sendo pouco especificado demais, pelo critério do autor, o que cria bugs de segurança em alguns alvos.
      No fim, os autores do padrão C não conseguem definir até o comportamento do hardware, apenas a semântica da linguagem; portanto, a área de criptografia inevitavelmente sofre com bugs cuja causa está no hardware.
    • O problema é que o comportamento indefinido em C e C++ é absurdamente amplo, e evitar tudo isso é extremamente difícil.
      Uma das vantagens do Rust é limitar possíveis comportamentos indefinidos a blocos unsafe. Ainda assim, mesmo que Rust tenha definido muitas coisas que em C seriam comportamento indefinido, ao entrar em código unsafe é muito fácil esbarrar sem querer em comportamentos indefinidos sutis.
    • Só há dois modelos de comportamento indefinido úteis para usuários de compiladores: rejeitar a compilação se for uma má ideia, ou fazer algo razoável e estável.
      Um terceiro modelo, que falha silenciosamente e gera código imprevisível, só é útil para autores de compiladores. Esconder-se atrás da especificação não traz benefício para usuários reais.
    • O texto de Russ Cox C and C++ Prioritize Performance over Correctness trata bem desse assunto: https://research.swtch.com/ub
    • Essa refutação está perto de atacar um espantalho. O ponto central é que os autores de compiladores decidem por conta própria o que é comportamento indefinido e definem o padrão de modo a obter mais margem para otimização.
      Essa otimização quebra código que antes funcionava bem. Os autores de compiladores poderiam priorizar compatibilidade retroativa, mas não fazem isso.
      Além disso, como essas otimizações nem melhoram de forma significativa o desempenho de código real, é preciso refutar o argumento de que esse trade-off de quebrar código não vale a pena.
  • Gosto do Bernstein, mas às vezes ele mira na direção errada e fica radical; este texto é um bom exemplo. No fim do texto, ele mesmo meio que admite isso.
    Uma grande parte do texto é sobre a questão secundária de quão bons são os ganhos de otimização, e mesmo com dados isso seria um julgamento que depende do caso de uso.
    A reclamação central é que compiladores C não levam em conta semânticas que não podem ser expressas pela linguagem, o que não deveria surpreender ninguém.
    No final, ele diz “use uma linguagem capaz de expressar a semântica necessária”, e o texto inteiro poderia ter sido substituído por essa única frase.

    • O ponto importante é que quem define a semântica de C e C++ joga comportamentos demais no cesto de “comportamento indefinido”.
      Uma boa parte disso tem justificativa duvidosa e torna mais difícil escrever programas corretos.
    • A parte de que os ganhos de otimização dependem do caso de uso trouxe um contexto útil e foi bastante esclarecedora.
    • Aqui, o DJB foi pouco convincente. Apareceu bastante uma visão religiosa elitista sem fundamento.
  • C e C++ não são adequados para escrever algoritmos com garantia de tempo constante.
    O padrão quase não tem noção de tempo real, e os compiladores também não oferecem garantias adicionais por meio de extensões.
    Mas culpar os desenvolvedores do compilador por isso é mirar no lugar errado.

    • Se você quer gerar código de máquina que sempre opere em tempo constante, independentemente de desvios, precisa usar uma linguagem capaz de expressar isso. C não dá suporte a isso.
    • Fico curioso sobre qual linguagem seria adequada para escrever algoritmos com garantia de tempo constante.
  • Em CPUs Intel, seja com clang ou qualquer outra coisa, não é possível gerar código correto em modo de usuário. Para começo de conversa, código correto nem existe.
    https://www.intel.com/content/www/us/en/developer/articles/t...
    Olhando o DOITM no documento, é simplesmente impossível que uma biblioteca criptográfica em espaço de usuário configure os bits necessários.

    • Código em modo de usuário também pode ser executado no modo correto. Ele só não consegue alternar esse modo por conta própria.
      Uma vez ativado, ele funciona bem também no espaço de usuário; portanto, por exemplo, poderia ser uma flag por processo ativada por uma chamada de sistema prctl, com o escalonador ajustando o MSR durante a troca de tarefas.
    • Não daria para fazer uma chamada de sistema ao kernel para configurar a flag e depois voltar ao modo de usuário nesse estado?
  • Só pela frase “sempre que possível, autores de compiladores se recusam a assumir responsabilidade pelos bugs que criaram”, é raro ver a credibilidade técnica de um post de blog desmoronar tão rapidamente
    Seguindo até o link, trata-se apenas de um ponto bem básico de C: comportamento indefinido não significa que ele produza “um valor arbitrário”

    • Parece que estão olhando para coisas diferentes e chamando-as de “bug”. Um lado fala de bug no código-fonte; o outro, de bug no programa gerado
      Mesmo havendo comportamento indefinido, muitas vezes o código-fonte é bugado, mas o programa gerado ainda está correto. Mais tarde, quando o autor do compilador adiciona uma nova otimização e, com base naquele comportamento indefinido, gera um programa com bug, começa a disputa sobre responsabilidade
      O ponto que não se quer admitir é que a responsabilidade perante o usuário é dividida entre todos os lados. Se uma aplicação CRUD dereferenciou NULL e, só por isso, a bateria pegou fogo, uma pessoa sensata não culparia apenas o autor da aplicação por ter esquecido uma verificação de NULL
      Compiladores, sistemas operacionais e fabricantes de hardware também precisam se responsabilizar por produtos projetados de forma irresponsável, e a expressão “comportamento indefinido” no padrão ISO não encerra a questão. Todos os integrantes da cadeia de suprimentos compartilham a responsabilidade de prever como o produto pode ser usado indevidamente e de lidar com isso de forma razoável
    • Acho que o autor sabe muito bem o que é comportamento indefinido. Ele apenas está olhando criticamente para o sistema como um todo
      Comportamento indefinido existe para entregar valor. É possível criar uma linguagem sem esse tipo de coisa; o motivo de ele existir é a portabilidade e a flexibilidade que dá aos autores de compiladores
      O ponto central do texto é se essa flexibilidade vale a pena quando comparada à dificuldade de escrever programas sem comportamento indefinido
      O autor entende que o dinheiro perdido com bugs parece ser maior do que o dinheiro economizado com bytecode mais rápido, e que, como os autores de compiladores têm muita influência sobre o que entra nos padrões da linguagem, há pouca disposição para corrigir isso
  • Como referência, o clang tem o atributo clang::optnone, que desativa todas as otimizações por função, e o GCC tem o excelente atributo gnu::optimize, que permite adicionar ou remover otimizações por nome, ou definir o nível de otimização independentemente das flags do compilador
    gnu::optimize(0) é parecido com aquela flag do clang. O clang também tem clang::no_builtins, que desativa especificamente otimizações de memcpy e memset

  • Tenho alguma simpatia pelos objetivos que o pessoal de criptografia quer, como avaliação em tempo constante e ocultação de valores secretos
    Mas compiladores de uso geral não pensam nisso na maior parte do tempo, então parece difícil que isso passe de um hack que funciona na maioria dos casos
    Para fazer isso a sério, provavelmente seria necessário um compilador especializado próprio, ou continuar indo para assembly

  • Algum dia, talvez vejamos o presente como os velhos tempos ruins e já tenhamos saído de C para uma linguagem com muito menos comportamento indefinido
    Em C, é fácil demais escrever expressões que compilam, mas cuja intenção o compilador simplesmente não tem como saber
    Por exemplo, em Python é possível escrever um código como result = [something(value) for value in set_object]. Como um objeto set não tem ordem, fica claro que a ordem de processamento dos itens e a ordem do resultado não importam, e isso abre espaço para muitas otimizações no nível da linguagem sem que o compilador precise adivinhar a intenção do autor
    Código semelhante em outras linguagens com dados imutáveis vai um passo além: como something(value1) não pode afetar something(value2), as execuções podem ocorrer em paralelo, seja em threads ou em processos
    Uma parte considerável da otimização dos compiladores C consiste em olhar padrões de código e encontrar formas de acelerar algo que provavelmente era a intenção do autor. Como C, em comparação com linguagens modernas, tem pouca capacidade de expressar intenção, há liberdade para inferir, mas, para entregar um desempenho decente, é preciso fazer esse tipo de dedução
    Ainda assim, pode ser uma bênção disfarçada, como o caso em que o telescópio Hubble precisou de óculos. Ao tentar superar limitações, criaram técnicas excelentes e, depois de corrigido o problema, essas técnicas renderam um desempenho muito maior do que se esperava originalmente. Aplicar otimizações de compiladores C a linguagens que não são C talvez funcione como um superpoder

    • A desvantagem do exemplo em Python é que, mesmo que a ordem não seja especificada, as pessoas podem depender de alguma propriedade, e, se o otimizador mudar a ordem, o código pode quebrar
      Basicamente é parecido com comportamento indefinido, mas pode aparecer não como um problema imediato de segurança, e sim como um resultado incorreto. Claro que um resultado incorreto pode levar a um problema de segurança mais tarde
      Ao contrário do comportamento indefinido, criar um “sanitizador” que verifique se o código funciona para todas as ordens possíveis de um set é, na prática, impossível
      gcc e clang têm muitos hints de baixo nível que frequentemente não existem em outras linguagens. Há __builtin_expect/__builtin_unpredictable, __builtin_unreachable/__builtin_assume, #pragma clang loop vectorize(assume_safety)/#pragma GCC ivdep, pragmas para desligar o desenrolamento de loops ou a vetorização, ou para escolher valores específicos, entre outros
      O que acho que mais falta é uma barreira de otimização que impeça explicitamente o compilador de inferir coisas com base na origem dos valores. __asm__ permite isso em certa medida, mas tem efeitos colaterais indesejados e exige nomes de tipos de registradores específicos de plataforma
      O potencial de otimizações de alto nível baseadas em intenção também é claro. Penso em coisas como reservar espaço em uma lista dinâmica antes de fazer push n vezes em um loop, fundir buscas em hashmap do tipo contains→get→put com a mesma chave, ou inferir localmente o comportamento de alocação global para eliminar objetos e alocações
    • Em teoria faz sentido, mas ninguém demonstrou na prática que isso seja mais rápido que C
      C fica suficientemente perto do hardware real para que o programador possa simplesmente dizer o que fazer, então o compilador não precisa adivinhar a intenção do programador
    • É verdade que há espaço para otimizações baseadas em semântica, mas, pelo que se observa, essas otimizações em geral ficam em torno da alocação de memória
      As linguagens que implementam esse tipo de otimização de memória costumam ser do tipo Java e, para começo de conversa, têm uma pessimizacão preventiva agressiva, o que cria motivação para fazer essas otimizações. Mesmo assim, essas otimizações não compensam a perda
      O ponto é que C também não é grande coisa, mas o outro lado é pior
  • Se você não gosta da semântica de C, não fique com raiva dos engenheiros de compiladores; use outra linguagem de programação

    • Para ser sincero, não sei se djb conseguiria tolerar qualquer coisa além do próprio qhasm. Até mesmo Zig. Essa avaliação vinda dele não é tão surpreendente
  • É um texto interessante, que traz um ponto de vista pouco ouvido. Também vale conferir: https://gavinhoward.com/2023/08/the-scourge-of-00ub/