2 pontos por GN⁺ 2024-10-03 | 1 comentários | Compartilhar no WhatsApp
  • Em situações de alta contenção, as diferenças entre implementações de mutex ficam bem evidentes, e o pthread_mutex_t da Cosmopolitan Libc apresenta tempo de execução menor e uso de CPU mais baixo do que as principais implementações do Windows e do Linux
  • Em um teste no Windows com um Threadripper 29070WX de 24 núcleos, a Cosmopolitan foi 2,75 vezes mais rápida que o Microsoft SRWLOCK e usou 18 vezes menos recursos de CPU
  • No Linux, em um Threadripper Pro 7995WX de 96 núcleos, ela foi 3 vezes mais rápida que a glibc e 11 vezes mais rápida que a musl libc, com uma diferença ainda maior em tempo de CPU
  • No MacOS M2 Ultra, a Apple Libc fica ligeiramente à frente, e a Cosmopolitan usa no ambiente ARM um algoritmo simples que depende da chamada de sistema ulock do XNU
  • A base do desempenho é a integração do nsync do Google; os pontos centrais são o caminho rápido com CAS, a fila de espera, futex/ulock/WaitOnAddress(), prevenção de starvation e o design com designated waker

Método do benchmark de mutex sob contenção

  • O teste cria 30 threads, e cada thread incrementa o mesmo inteiro global g_chores 100.000 vezes
  • Cada incremento é executado dentro de uma seção crítica muito pequena entre pthread_mutex_lock() e pthread_mutex_unlock()
  • As medições são em microssegundos e distinguem três tipos de tempo
    • wall time: o tempo real gasto na execução do programa, incluindo o overhead de criação das threads e de join
    • user time: tempo de CPU consumido no espaço de usuário
    • system time: tempo de CPU consumido no kernel
  • Como várias threads são executadas em paralelo, a soma de user time e system time pode ser maior que o wall time
  • Em situações sem contenção, a diferença de desempenho entre implementações costuma ser pequena, mas em situações de contenção as diferenças de design do mutex ficam bem evidentes

Windows: Cosmopolitan mais rápida que SRWLOCK

  • O teste no Windows foi realizado em um Threadripper 29070WX de 24 núcleos
  • O MutexShootout, de Mark Waterman, avaliou o SRWLOCK do Windows como a implementação mais forte em cenários de alta contenção
  • Nas mesmas condições, o pthread_mutex_t da Cosmopolitan registrou wall time menor e uso de CPU mais baixo que o SRWLOCK
Implementação wall time user time system time
Cosmopolitan pthread_mutex_t 148.940µs 328.125µs 62.500µs
Microsoft SRWLOCK 410.416µs 5.515.625µs 1.640.625µs
Microsoft CRITICAL_SECTION 949.187µs 7.937.500µs 5.078.125µs
MSVC 2022 std::mutex 991.750µs 12.156.250µs 4.031.250µs
spin lock 1.165.435µs 24.515.000µs 15.000µs
Cygwin pthread_mutex_t 9.780.803µs 1.937.000µs 6.156.000µs
  • O mutex da Cosmopolitan é 2,75 vezes mais rápido que o Microsoft SRWLOCK e usa 18 vezes menos recursos de CPU
  • Em comparação com o mutex da Cygwin, que oferece uma implementação POSIX no Windows, ele é 65 vezes mais rápido
  • Nesse caso de uso, o mutex da Cygwin apresentou resultado mais lento até que um spin lock

Linux: diferença em tempo de CPU ainda maior que em wall time

  • O teste no Linux foi realizado em um Threadripper Pro 7995WX de 96 núcleos
Implementação wall time user time system time
Cosmopolitan pthread_mutex_t 36.905µs 44.511µs 23.492µs
glibc pthread_mutex_t 101.353µs 150.706µs 2.724.851µs
spin lock 202.423µs 4.694.749µs 2.000µs
Musl libc pthread_mutex_t 411.013µs 2.167.898µs 9.926.850µs
  • O mutex da Cosmopolitan é 3 vezes mais rápido que a glibc e 11 vezes mais rápido que a musl libc
  • Em termos de tempo de CPU, usa 42 vezes menos que a glibc e 178 vezes menos que a musl libc
  • Em workloads nos quais todas as threads precisam realizar trabalho serializado, a Cosmopolitan pode parecer, no htop, como se apenas um núcleo estivesse ativo
  • Na mesma situação, glibc e musl libc podem ocupar muito a CPU, aumentando a carga ao executar várias tarefas no mesmo servidor

MacOS: Apple Libc fica ligeiramente à frente

  • O teste no MacOS foi realizado em um M2 Ultra
Implementação wall time user time system time
Apple Libc 52.263µs 43.202µs 911.009µs
Cosmopolitan pthread_mutex_t 54.700µs 63.055µs 1.003.674µs
  • No MacOS M2 ARM64, a Apple Libc é um pouco mais rápida que o mutex da Cosmopolitan
  • A implementação geral de mutex da Cosmopolitan não funciona bem nessa plataforma
  • No MacOS ARM, a Cosmopolitan usa um algoritmo mais simples baseado em Futexes Are Tricky, de Ulrich Drepper
  • Essa abordagem deixa a maior parte do trabalho pesado para a chamada de sistema ulock do XNU e, como resultado, entrega desempenho quase igual ao da implementação da Apple

Base do desempenho: integração do nsync

  • O ponto central do desempenho do mutex da Cosmopolitan é a integração da biblioteca nsync, do Google
  • nsync é uma biblioteca com 371 estrelas no GitHub, escrita por Mike Burrows, do Google
  • No processo de integração à Cosmopolitan, foram feitos os seguintes trabalhos
    • Encontrar e corrigir um bug antigo, até então não detectado, na função de unlock do mutex do nsync
    • Fazer o port para AARCH64 usando operações atômicas C11, tornando o mutex nsync sob contenção 30% mais rápido que o nsync upstream
    • Reescrever integrações de sistema como futex para viabilizar portabilidade em runtime
    • Fazer com que funcionasse de forma fluida com cancelamento de threads POSIX

Como o nsync funciona

  • Para adquirir o lock rapidamente, o nsync primeiro tenta imediatamente um CAS otimista (compare and swap)
  • Se não conseguir adquirir o lock, ele adiciona a thread chamadora a uma lista duplamente ligada de waiters
    • Cada waiter tem seu próprio semáforo em uma cacheline separada e independente
    • Uma thread que entrou em estado de espera não mexe mais no lock principal
    • Isso é importante para reduzir o overhead de comunicação que surge quando vários núcleos acessam a mesma cacheline
    • Como contexto relacionado, há o link para What Every Programmer Should Know About Memory, de Ulrich Drepper
  • O nsync usa o futex do sistema operacional para colocar threads para dormir
    • No MacOS, futex é chamado de ulock
    • No Windows, WaitOnAddress() faz o papel de futex
    • Entre os sistemas operacionais compatíveis com a Cosmo, apenas o NetBSD não tem futex; os semáforos POSIX são implementados no espaço do kernel e exigem um novo descritor de arquivo para cada semáforo
  • O nsync evita starvation com o conceito de “long wait”
    • Se um waiter for acordado 30 vezes, mas sempre falhar internamente ao tentar adquirir o lock, um bit é adicionado ao lock para impedir que threads que ainda não esperaram consigam adquiri-lo
    • Quando esse bit está presente, o CAS inicial de threads recém-chegadas falha até que a fila de espera seja esvaziada até certo ponto
  • Casos de uso com contenção em seções críticas pequenas ficam mais rápidos com o conceito de designated waker
    • Quando uma thread acorda e tenta adquirir o lock, um bit é definido no lock principal
    • No nsync, a função de unlock tem a responsabilidade de acordar a próxima thread em espera
    • Graças a esse bit, a thread que está fazendo unlock não precisa acordar um segundo waiter quando já há uma thread acordada
  • O código-fonte relacionado está em cosmopolitan/third_party/nsync/mu.c e cosmopolitan/libc/intrin/pthread_mutex_lock.c

Serviço real e código de validação

  • Como demo ao vivo usando o Cosmo Mutex, é possível ver o servidor http://ipv4.games/
  • Esse serviço roda em uma VM GCE de 2 núcleos e, até agora, já suportou um DDoS de botnet com até 49.131.669 IPs
  • Graças ao nsync, foi possível mover consultas SQL para threads em segundo plano e usar uma arquitetura em que as threads enviam mensagens umas às outras
  • Os indicadores de status podem ser verificados em /statusz
  • O código do benchmark mede wall time com gettimeofday() e mede user time e system time com getrusage()
  • Ao final, ele verifica g_chores == THREADS * ITERATIONS para validar que todos os incrementos foram executados

Cuidados ao analisar spin locks

  • Em situações sem contenção, as diferenças entre implementações de mutex são pequenas, e um spin lock de poucas linhas pode até ser melhor
  • Mas spin locks só devem ser usados quando realmente não houver outra opção
  • Eles são úteis em lugares onde restrições de nível extremamente baixo, como no kernel, dificultam o uso de abordagens mais complexas
  • Spin locks também podem ser usados como detalhe interno de implementação de um lock do nsync
  • Se o desempenho do lock for analisado apenas pelo wall time, um spin lock pode parecer bom; portanto, é preciso verificar também o tempo de CPU com getrusage()

1 comentários

 
GN⁺ 2024-10-03
Opiniões do Hacker News
  • Novas implementações de mutex e comparações são sempre interessantes, mas não gosto desta metodologia de benchmark. Parece quase um microbenchmark
    Quem realmente coloca locks rápidos em produção normalmente usa programas multithread muito grandes como principal meio de teste de desempenho. Em cargas de trabalho complexas, nas quais o tamanho da seção crítica, o número de threads concorrentes e o grau de contenção variam, os fatores que tornam um mutex rápido ou lento parecem mudar
    Para referência, escrevi o lock rápido do WebKit, inventei a abstração ParkingLot para implementação de locks (também usada no Rust e na Unreal Engine) e, no passado, fiz pesquisa e publiquei um artigo sobre locks rápidos para Java

    • Como alguém que criou apps desktop, eu acrescentaria que, em um app com dezenas de threads rodando com frequência, gostaria de ver números de desempenho para o caso sem muita contenção
      Como programador de áudio em tempo real, o custo de adquirir um mutex que ainda não está bloqueado é mais importante. No nosso app, essa situação é de longe a mais comum. Da mesma forma, eu também gostaria de saber o custo de uma operação try-lock que vai falhar, não apenas quando N threads competem
      Como o Cosmopolitan é open source, eu poderia medir isso por conta própria, mas ainda assim sinto falta
    • Tive a mesma impressão. Há vários tipos de mutex, e alguns são melhores para determinadas cargas de trabalho. DistributedMutex e SharedMutex vêm à mente (https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...)
      Assim como com hash maps, é raro um único hash map ser melhor para todas as cargas de trabalho possíveis
    • Esse estilo de mutex também deverá ser usado no PyMutex do Python 3.13. Há benchmarks reais mostrando quanto o PyMutex é mais rápido que o PyThread_type_lock anterior à versão 3.13
    • É certamente um microbenchmark, e é bem provável que não represente o desempenho em geral. Esta página apresenta um bom padrão de práticas para benchmarking de sistemas operacionais. Mas é um pouco mais voltada ao meio acadêmico: https://gernot-heiser.org/benchmarking-crimes.html
    • Esse benchmark específico pode, na verdade, favorecer comportamentos indesejáveis, por exemplo injustiça patológica. O escalonamento ideal seria executar todas as operações de incremento da primeira thread, depois todas as da segunda, e assim por diante, porque isso minimizaria o tráfego entre processadores
      Um mutex que, ao falhar em adquirir o lock, dorme por um tempo fixo (por exemplo, 100 µs) quase sempre vai agrupar o trabalho, aproximando-se desse comportamento e podendo “vencer” no benchmark. Mas, em aplicações reais, se houver qualquer contenção, um mutex assim é horrível
      Não estou dizendo que este mutex seja ruim ou que o mutex pthread seja bom, e sim que esse microbenchmark não mede algo que sirva para prever o desempenho de aplicações reais
  • Na parte que diz que “o Cosmopolitan Mutex é bom porque usou uma biblioteca chamada nsync”, eu nunca tinha ouvido falar de nsync, mas Mike Burrows também escreveu a implementação de mutex de produção do Google: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    Por isso fiquei curioso sobre por que essa implementação de mutex ficou fora do benchmark. E, se no macOS ela delega para __ulock, parece que daria para conseguir isso de forma mais simples usando apenas as funções-membro wait() e notify_one() da biblioteca atomic do libc++
    Também houve uma thread grande, algum tempo atrás, relacionada à melhoria da implementação de mutex do Rust: https://github.com/rust-lang/rust/issues/93740#issuecomment-... O interessante é que o funcionamento interno de quase todas as implementações populares de mutex é discutido em detalhes

    • Quando entrei na AV, Mike já era uma lenda. Havia a história de que, sempre que o mecanismo de busca precisava ficar mais rápido, ele vinha, reescrevia algumas funções centrais e voltava ao que estava fazendo antes
      Pode ser verdade, mas não tenho como confirmar diretamente. Era um engenheiro extremamente inteligente, focado em eficiência. Só que nós não mantínhamos um servidor rodando por muito tempo
    • Burrows também esteve envolvido na transformada de Burrows-Wheeler, no Bigtable, no Dapper, no Chubby etc.
    • Essa thread do Rust acaba chegando lá, mas basicamente é sobre o trabalho da Mara, e por isso o livro dela, lançado em janeiro de 2023, também é mencionado
      A implementação atual de mutex do Rust entrou no início deste ano e, no Linux, talvez não seja muito diferente, mas entendo que no Windows e no Mac é um trabalho novo
      Ainda assim, a explicação da Mara sobre o interior de outras implementações continua interessante, mas é melhor verificar se a informação não está desatualizada para o seu caso
    • O motivo de a implementação de mutex do Abseil ter ficado fora do benchmark pode ser o fato de ela ser uma implementação em C++, não em C. É só um palpite
    • Acho que Mike Burrows também recebeu um prêmio da ACM, e há até uma foto dele lá
      https://awards.acm.org/award-recipients/burrows_9434147
  • A frase “Ainda é uma nova biblioteca C, então tem arestas, mas está melhorando tão rápido que não usá-la em produção começou a parecer uma negligência profissional” é bem estranha. Eu valorizo muito o projeto Cosmopolitan, mas esse tipo de afirmação exagerada de superioridade costuma ser um sinal de alerta bem ruim

    • Acho que as afirmações da Justine em geral estão corretas. Só que usar exageros e expressões de autopromoção parece ser o estilo dela, ou talvez sua personalidade
      Entendo que isso possa parecer áspero para algumas pessoas. Já houve drama desse tipo antes no llamacpp
    • Justine parece ser uma pessoa bastante brilhante e criativa, mas eu não gostaria de usar em produção uma libc “nova” e “com arestas”
      Em produção, a prioridade principal não é “melhorar incrivelmente rápido”, e sim estabilidade, previsibilidade e confiabilidade. Claro que desempenho também é importante. Código mais rápido pode reduzir infraestrutura, o que é bom em termos de custo e meio ambiente. Mas velocidade vem por último
    • Quando alguém passa muito tempo sozinho programando na frente do computador, talvez a falta de contato social acabe trazendo certo grau de arrogância. Sem mecanismos para colocar em perspectiva a importância da própria pessoa ou do próprio trabalho, os resultados podem parecer grandiosos além do reconhecimento amplo que realmente têm, mesmo sendo impressionantes
      Por exemplo, APE me parece um hack muito impressionante, mas também dá para criticar dizendo: “então agora, em vez de ser inseguro em uma única plataforma, pode ser inseguro em várias plataformas ao mesmo tempo?”
      Quanto mais tempo passo na área de tecnologia, mais percebo que benefícios totalmente mútuos são extremamente raros; na maioria dos casos há ganhos e perdas ao mesmo tempo, ou seja, trade-offs
    • Pelo menos para mim, pareceu uma piada
    • Fico curioso se você já considerou que seu senso de humor e o da Justine podem ser diferentes. Também não sei a quem você acha que ajuda trazendo isso aqui
  • Fugindo completamente do assunto, como desenvolvedor de jogos passei a gostar de mutexes lentos que fazem bastante trabalho de debug em todas as builds de desenvolvimento. Coisas como ter nome/ID de debug, rastrear o proprietário, reportar ao profiler o tempo gasto em contenção e também reportar ao profiler as mudanças de propriedade
    Jogos tendem a estruturar a concorrência de outra forma, e também evoluíram padrões para evitar locks. Mas esses padrões são difíceis de usar e exigem que o programador mude a estrutura. A maior parte do código começa com “vamos colocar um lock aqui por enquanto e passar pelo milestone”
    Locks rápidos também podem ficar imprevisivelmente lentos e quebram qualquer garantia de tempo real que existisse. Em média podem ser rápidos, mas a latência de cauda não desaparece. Eu não quero ser a pessoa que volta para investigar “nosso jogo está engasgando”, mas normalmente acabo sendo essa pessoa
    Então prefiro usar locks lentos. Aqueles que aparecem bem grandes e em vermelho no profiler. Se der para ver que algo está apanhando, é só refatorar e eliminar
    Sei que é uma exigência difícil. Em uma produção AAA, dá para contar nos dedos as pessoas que sabem usar um profiler. Em várias produções que vi, sempre foi assim
    Desculpem o desabafo, mas espero que a pesquisa em primitivas e algoritmos de concorrência rápidos continue

    • Indo ainda mais para a tangente, este é um dos motivos pelos quais desenvolver jogos em Rust é prazeroso
      Em jogos, você nunca quer contenção de locks se puder evitar, e em muitos casos dá para provar que pegar um lock é desnecessário. Por exemplo, cada frame é dividido em etapas, e o acesso mutável a algum recurso compartilhado só é necessário em uma etapa específica, como update() antes de render() ou hot reload de assets
      Com threads com escopo e as regras de empréstimo do Rust, dá para estruturar o código de modo que mutexes nem sejam necessários, e ter confiança de que, se uma mudança futura no código os tornar necessários, o compilador vai apontar um erro de forma rigorosa
      Sempre que possível, prefiro receber um erro de compilação a um pico no profiler
    • Concordo totalmente. Recursos de depuração como detecção de deadlock ou inspeção de estado interno se pagam facilmente. Se você está adquirindo locks com frequência suficiente para impactar o desempenho, precisa rever o design. Compartilhar estado mutável entre threads deve ser evitado
  • Por um lado, a linhagem Cosmo/APE/redbean parece realmente incrível, e os comentários nos textos relacionados também são em geral positivos, com pouca coisa contestando o conceito em si. Mas, por outro lado, quase nunca ouço falar de outras pessoas usando isso
    Nem todo mundo compartilha amplamente o próprio trabalho, mas, depois de alguns anos, eu esperaria ter visto pelo menos alguns posts de retrospectiva de projetos. Todas as menções a Cosmo/APE/redbean que vi vieram do site da Justine
    Então fico curioso. Há alguma armadilha escondida? São ferramentas que fazem alguma coisa ruim para obter resultados? É algum tipo de piada ou trollagem à la tom7 que eu não entendo por não conhecer profundamente compiladores ou runtimes? Ou são, de fato, ferramentas engenhosas que ainda não se disseminaram amplamente?

    • APE funciona por meio de um truque engenhoso que pode ser bloqueado a qualquer momento, e no OpenBSD isso de fato aconteceu
      A maioria das pessoas que cria software multiplataforma não quer um único executável que rode em todas as plataformas, mas sim uma única base de código que funcione corretamente em cada plataforma suportada
      Desse ponto de vista, linguagens como Go, nas quais é possível compilar de forma cruzada para todos os alvos evitando CGO, são agradáveis. Mas a mágica do APE de ser executável de três maneiras, por mais inteligente que seja, não inspira confiança de que vá funcionar para sempre, e para a maioria das pessoas também não traz muito ganho prático
      Cada plataforma tem seus próprios requisitos de empacotamento e assinatura, então é melhor compilar separadamente para os alvos específicos de cada plataforma
    • Pessoalmente, cosmo e ape parecem muito inteligentes, mas, se as ferramentas comuns já funcionam bem, não preciso desse tipo de esperteza no trabalho
      Por exemplo, se eu já consigo fazer cross-compile de um projeto para outros sistemas operacionais e plataformas, ou se já tenho essa infraestrutura de build, não há motivo para procurar uma solução que gere um único binário que funcione em qualquer lugar
      Além disso, o APE usa hacks engenhosos para rodar em vários sistemas operacionais. E se esses hacks quebrarem um dia, à medida que os formatos de executáveis evoluírem? E se ninguém tiver tempo para corrigir o APE para acompanhar essa mudança?
      Por outro lado, as ferramentas entediantes como gcc, clang, go e rust continuarão sendo atualizadas e funcionando em sistemas operacionais que evoluem. Por isso acabo ficando simplesmente com o lado entediante. O motivo de eu não me preocupar com o que é engenhoso é que o que é entediante simplesmente funciona bem para mim
    • O llamafile da Mozilla usa isso. Ele junta os pesos do modelo e o executável em um só arquivo, para que possa rodar em qualquer plataforma cosmo/ape, e também sobe um servidor HTTP redbean para interação
      Também é possível executá-lo sem os pesos embutidos e fazer com que leia os pesos do sistema de arquivos. Pode ser a forma mais fácil de “baixar e executar imediatamente” um LLM local
    • Cosmopolitan sempre me pareceu uma brecha técnica que rende posts de blog interessantes. É o tipo de coisa que quase garante a primeira página em lugares como o HN só pela engenhosidade e pela obsessão com configuração
      Mas, para ser usado como tecnologia de base, como libc, parece útil principalmente como um brinquedo divertido ou para pequenos projetos pessoais
      Nesse contexto, soa um pouco estranho quando é apresentado como uma alternativa séria a coisas como glibc, musl e msvcrt. É um hack muito simpático, mas eu ficaria bastante desconcertado se o encontrasse em algo de que dependo seriamente
    • A Mozilla tem o projeto Llamafile baseado na Cosmopolitan libc: https://github.com/Mozilla-Ocho/llamafile
      No Hugging Face, eles também publicam regularmente modelos populares reempacotados nesse formato: https://huggingface.co/models?search=llamafile
      Mas se isso tem utilidade prática além de testar rapidamente modelos pequenos é outra questão
  • Se é tão bom assim, fico me perguntando por que todas as bibliotecas C não adotaram o mesmo truque
    Meu palpite é que esses truques provavelmente só são sempre rápidos em uma arquitetura específica, em um modelo específico de CPU, com uma carga de trabalho ou padrão de acesso específico. Se fizerem benchmarks adequados com cargas de trabalho variadas em todo o hardware suportado, talvez o mesmo benefício não apareça
    Ou talvez a semântica da API pthread que a Cosmopolitan tenta implementar seja sutilmente diferente, e esta implementação talvez não cumpra rigorosamente a especificação
    É difícil imaginar que os vários autores de libc não estejam acompanhando as pesquisas mais recentes sobre primitivas de sistema operacional

    • Esses projetos têm dezenas de prioridades além de uma API específica. Ficar obcecado com uma API individual não é uma boa forma de gastar tempo limitado. E, como contraexemplo, basta olhar para malloc e as rotinas de strings nas libcs comuns do Linux
      O malloc da glibc é razoavelmente utilizável, mas perde fácil para alternativas mais modernas em velocidade geral e escalabilidade. Ele fragmenta bastante e piora com o tempo, e há muitos ajustes como MALLOC_ARENA_MAX que têm grande impacto em cargas de trabalho reais. O malloc da musl é terrível em todos os níveis em termos de desempenho. Usar o alocador da musl em um programa multithread prejudicava tanto o desempenho que quase dava para chamar de negligência
      A musl também não tem coisas como rotinas de comparação de strings otimizadas com SIMD. Você ficaria surpreso com quantos ciclos de CPU são gastos nesse tipo de operação em programas não triviais; isso aparece claramente nos perfis reais, e melhorar isso beneficia quase todos os programas de forma geral. As rotinas otimizadas da glibc são boas, mas ainda parecem poder ser mais rápidas
      Essas coisas não são “otimizações especializadas para uma arquitetura só e que não generalizam”. Em especial, essas duas áreas são bem exploradas e compreendidas, reduzem o tempo de relógio em 2 a 5 vezes em quase todas as cargas de trabalho e também melhoram bastante o uso do conjunto de trabalho em execuções longas. Então por que não foram adotadas? Como sempre, provavelmente porque havia outras coisas a fazer ou porque, como na musl, existiam prioridades conflitantes que favoreciam simplicidade em vez de desempenho máximo
      Não estou culpando esses projetos. Ninguém diz “meu programa é horrivelmente lento, foi projetado para não fazer nada direito, e tenho orgulho disso”. Mas a ideia de que as pessoas que trabalham nesses projetos escolheram apenas projetos perfeitamente na fronteira de Pareto não é nada realista e não captura como a maioria dos projetos realmente funciona
    • Pode ser por política, síndrome de NIH ou mantenedores antigos
      Mudar qualquer coisa na glibc, ou no equivalente do lado do C++, leva uma eternidade
      Existem vários tipos de primitivas de sincronização, e pthreads só dá suporte a algumas delas. Ao se limitar a isso, em geral você abre mão de desempenho em troca de portabilidade
    • Fico me perguntando se “é difícil imaginar que os vários autores de libc não estejam acompanhando as pesquisas mais recentes sobre primitivas de sistema operacional” foi sarcasmo
      Não sei quanto aos mantenedores de libc, mas, como alguém que mantém algumas coisas, eu não tento implementar as pesquisas mais recentes. Tento manter a estabilidade e garantir que o desempenho seja aceitável. Implementações de pesquisa ficam fora do meu orçamento de “manutenção”
    • Fico me perguntando se há considerações de ABI ao mudar a implementação de mutexes pthread
    • A pergunta “se é tão bom assim, por que todas as bibliotecas C não adotaram o mesmo truque?” me lembra esta piada
      Um homem e um estatístico estão andando pela rua quando veem uma nota de 50 euros. O estatístico continua andando, e o homem para e diz: “Olhe, tem dinheiro no chão”. Então o estatístico diz: “Deve ser falsa. Se fosse verdadeira, alguém já teria pegado”, e continua andando. O outro homem pega o dinheiro
  • Threads e mutexes estão entre os elementos que mais introduzem complexidade na ciência da computação. Sempre olho uma nova implementação com ceticismo até que ela tenha sido usada em grande escala por anos
    Bugs nesse tipo de mecanismo de threading muitas vezes escapam até das revisões mais intensas. Quando o Java apareceu em meados dos anos 90, expôs todo tipo de bug de threads e mutexes no Solaris
    Não precisamos da implementação de mutex mais rápida, precisamos de uma implementação confiável

    • Mutexes estão longe de ser a coisa mais “complexa”. Também não há tantas formas de implementá-los com eficiência. Na maioria dos casos, especialmente no caminho de leitura, é melhor evitá-los
  • Este código não faz benchmark do desempenho de lock de mutex, mas sim da contenção de mutex. Se você está usando locks desse jeito, deveria reavaliar seu código
    Cada thread bloqueia e desbloqueia o mutex toda vez que incrementa g_chores. Isso gera overhead de aquisição e liberação frequentes do mutex, repetido 100.000 vezes por thread
    Esse overhead mascara as diferenças reais de desempenho entre os mecanismos de lock. O benchmark é dominado pela contenção de lock, não por trabalho real. Um benchmark assim é inútil

  • Sou fã da Justine e do trabalho dela, mas este provavelmente é o caso de teste menos interessante para um benchmark de mutex. Várias threads martelando continuamente o mesmo mutex é algo que deveria ser evitado desde o início
    Por isso, não acho muito interessante saber qual implementação de mutex lida melhor com esse caso

    • Fico curioso sobre o que você considera bons casos de teste para benchmark de mutex
    • Na maioria das situações em que uso locks ou semáforos, é em torno de um recurso muito caro. O uso desse recurso supera o overhead de desempenho do lock
    • Então o que deveríamos medir? O caso sem contenção é importante e serve como linha de base, mas, fora isso, é justamente aí que está o ponto fraco de um mutex. Se ele não lida bem com contenção, o hardware fica ocioso, o escalonador trabalha mais ou há mais entradas no kernel
      Uma coisa importante que deixei de mencionar é que, sob contenção, um lock com desempenho ruim pode causar efeitos sistêmicos muito negativos, como criar hotspots na rede de memória, e isso também apareceria aqui
    • É difícil concordar totalmente com a avaliação de que “várias threads não deveriam ficar martelando o mesmo mutex”
      Consigo pensar em alguns casos em que várias threads convergem para o mesmo mutex. Um exemplo simples é preencher simultaneamente uma estrutura de dados como uma lista ou um dicionário
      Também dá para fazer isso com passagem de mensagens, mas pode usar mais memória e ser mais lento do que esperar para escrever em um local compartilhado
  • Produção não tem a ver com velocidade, eficiência ou hacks obviamente “engenhosos”
    Se eu tiver que sacrificar 50% de eficiência para ter a garantia de que não serei chamado para consertar um sistema quebrado às 3 da manhã de domingo, faço essa escolha toda vez
    Produção tem a ver com confiabilidade, e escrever código confiável é 10 vezes mais difícil do que escrever código “rápido”