2 pontos por GN⁺ 2025-02-14 | 1 comentários | Compartilhar no WhatsApp

Existe uma forma de melhorar a velocidade do FFI no CRuby?

  • Quando for necessário chamar código nativo a partir de Ruby, o ideal é escrever o máximo possível em Ruby. Isso porque o YJIT consegue otimizar código Ruby, mas não consegue otimizar código C.
  • Ao chamar bibliotecas nativas, é melhor fazer a maior parte do trabalho em Ruby e escrever uma extensão nativa que forneça uma API simples para a chamada de funções nativas.
  • O FFI não oferece o mesmo desempenho de uma extensão nativa. Por exemplo, ao encapsular a função C strlen via FFI, o desempenho fica inferior em comparação com uma extensão C.

Resultados do benchmark

  • Chamar String#bytesize diretamente é o mais rápido, e isso pode ser considerado a linha de base.
  • A chamada de strlen por meio de uma extensão C é a segunda mais rápida, e a chamada indireta de String#bytesize vem logo depois.
  • A implementação com FFI é a mais lenta. Isso mostra que há um overhead considerável ao chamar funções nativas via FFI.

Dá para mudar essa realidade?

  • A partir de uma ideia de Chris Seaton, está sendo explorada a possibilidade de gerar código JIT para chamar funções externas.
  • No exemplo de wrapper com FFI, ao chamar attach_function, é possível gerar o código de máquina necessário no momento em que a função wrapper é definida.

Uso do RJIT

  • O RJIT é um compilador JIT escrito em Ruby e distribuído junto com o Ruby.
  • O RJIT foi extraído como gem para permitir que compiladores JIT de terceiros façam o mapeamento das estruturas de dados do Ruby com mais facilidade.
  • A ideia é sempre executar o ponteiro da função de entrada do JIT para que JITs de terceiros possam se registrar no código de máquina.

Prova de conceito

  • Com uma pequena prova de conceito chamada "FJIT", é possível gerar código de máquina em tempo de execução para chamar funções externas.
  • Nos benchmarks, o código de máquina gerado pelo FJIT é mais rápido que uma extensão C e mais de 2 vezes mais rápido que chamadas via FFI.

Conclusão

  • Isso mostra a possibilidade de escrever o máximo possível em Ruby mantendo a mesma velocidade de extensões C, ou até uma velocidade maior.
  • O Ruby pode passar a ter a vantagem de chamar código nativo sem depender de FFI.

Observações

  • No momento, isso está limitado à plataforma ARM64. É necessário adicionar um backend para x86_64.
  • Nem todos os tipos de parâmetros e de retorno são tratados. Atualmente, só é possível lidar com um único parâmetro e um único retorno.
  • É preciso executar o Ruby com as flags --rjit --rjit-disable. Isso deve ser resolvido quando a funcionalidade de Kokubun for aplicada.
  • Atualmente, isso só pode ser executado no Ruby head.

1 comentários

 
GN⁺ 2025-02-14
Comentários do Hacker News
  • Foi preciso lidar bastante com FFI para chamadas de função entre o Java Constraint Solver (Timefold) e o CPython

    • Os problemas de desempenho de FFI surgem principalmente do uso de proxies para a comunicação entre a linguagem hospedeira e a estrangeira
    • Chamadas diretas de FFI usando JNI ou a nova foreign interface são rápidas, com velocidade parecida à de chamar métodos Java diretamente
    • Porém, os coletores de lixo do CPython e do Java não combinam bem, então são necessárias técnicas especiais para sincronização
    • Ao usar proxies como JPype ou GraalPy, há sobrecarga de desempenho, pois é preciso converter parâmetros e valores de retorno, além de poder haver chamadas adicionais de FFI
    • Quando um objeto do CPython é passado para o Java, o Java mantém um proxy desse objeto do CPython
    • Se esse proxy for passado de volta para o CPython, é criado um proxy do proxy
    • Como resultado, os proxies do JPype são 1402% mais lentos do que chamar o CPython diretamente via FFI, e os proxies do GraalPy são 453% mais lentos
    • No fim, o bytecode do CPython foi convertido em bytecode Java, e foram criadas estruturas de dados Java correspondentes às classes do CPython utilizadas
    • Como resultado, foi obtido um ganho de desempenho 100 vezes maior do que com o uso de proxies
    • Converter ou ler bytecode do CPython é muito instável e pouco documentado, e por causa de várias peculiaridades da VM é difícil mapeá-lo diretamente para outro bytecode
    • Mais detalhes podem ser vistos no post do blog: link
  • Graças ao Rails At Scale e ao blog do byroot, este é um ótimo momento para se interessar por discussões aprofundadas sobre os internals e o desempenho do Ruby

    • Com as melhorias recentes no Ruby e no Rails, este é um bom momento para ser Rubyist
  • Pergunta sobre se seria possível compilar o código com JIT para chamadas de funções externas, em vez de chamar uma biblioteca de terceiros

    • Tenho quase certeza de que esse é o princípio básico do FFI do LuaJIT: link
    • Acho que essa é a razão de o FFI do LuaJIT ser tão rápido
  • Informação sobre uma biblioteca que usa JVMCI para gerar código arm64/amd64 na hora e chamar bibliotecas nativas sem JNI: link

  • Opinião de que "escreva o máximo possível em Ruby, especialmente porque o YJIT consegue otimizar código Ruby, mas não código C"

    • Fica a dúvida se Ruby não é uma linguagem bastante lenta
    • Se for para entrar no nativo, eu gostaria de fazer o máximo possível no nativo
  • Uso Ruby há mais de 10 anos, e é muito interessante ver os avanços recentes

    • Animador
  • Dúvida sobre por que a compilação JIT é necessária

    • Se dá para escrever em C, não seria possível compilar no momento do carregamento?
  • FFI - Foreign Function Interface, ou seja, a forma de chamar C a partir do Ruby

  • Pergunta se não é exatamente isso que a libffi faz

  • Acho que dá para entender por que não foram para o tenderlovemaking.com