2 puntos por GN⁺ 2025-02-14 | 1 comentarios | Compartir por WhatsApp

¿Hay una forma de mejorar la velocidad de FFI en CRuby?

  • Cuando necesitas llamar código nativo desde Ruby, es mejor escribir la mayor cantidad posible de código en Ruby. Esto se debe a que YJIT puede optimizar el código Ruby, pero no puede optimizar código C.
  • Al llamar bibliotecas nativas, conviene hacer la mayor parte del trabajo en Ruby y escribir una extensión nativa que ofrezca una API simple para invocar funciones nativas.
  • FFI no ofrece el mismo rendimiento que una extensión nativa. Por ejemplo, si envuelves la función C strlen con FFI, el rendimiento es inferior en comparación con una extensión C.

Resultados del benchmark

  • Llamar directamente a String#bytesize es lo más rápido, y puede considerarse como el punto de referencia.
  • La llamada a strlen mediante una extensión C es la segunda más rápida, y después viene llamar indirectamente a String#bytesize.
  • La implementación con FFI es la más lenta. Esto muestra que hay una sobrecarga considerable al invocar funciones nativas a través de FFI.

¿Se puede cambiar esta realidad?

  • A partir de una idea de Chris Seaton, se está explorando la posibilidad de generar código JIT para llamar funciones externas.
  • En el ejemplo del wrapper FFI, al llamar attach_function, se puede generar en ese momento el código máquina necesario al definir la función wrapper.

Uso de RJIT

  • RJIT es un compilador JIT escrito en Ruby que se incluye junto con Ruby.
  • Se extrae RJIT como gem para que compiladores JIT de terceros puedan mapear fácilmente las estructuras de datos de Ruby.
  • Siempre se ejecuta el puntero de función de entrada del JIT para permitir que un JIT de terceros se registre en el código máquina.

Prueba de concepto

  • Con una pequeña prueba de concepto llamada "FJIT", es posible generar código máquina en tiempo de ejecución para llamar funciones externas.
  • Según los benchmarks, el código máquina generado por FJIT es más rápido que una extensión C y más de 2 veces más rápido que una llamada FFI.

Conclusión

  • Esto demuestra la posibilidad de escribir la mayor cantidad posible de código en Ruby mientras se mantiene la misma velocidad que una extensión C (o incluso una velocidad mayor).
  • Ruby podría tener la ventaja de llamar código nativo sin FFI.

Advertencias

  • Por ahora está limitado únicamente a la plataforma ARM64. Es necesario agregar un backend x86_64.
  • No maneja todos los tipos de parámetros ni todos los tipos de retorno. Solo puede manejar un único parámetro y un único valor de retorno.
  • Es necesario ejecutar Ruby con los flags --rjit --rjit-disable. Esto probablemente se resolverá cuando se aplique la funcionalidad de Kokubun.
  • Por ahora solo puede ejecutarse en Ruby head.

1 comentarios

 
GN⁺ 2025-02-14
Comentarios en Hacker News
  • Tuve que trabajar bastante con FFI para llamadas de funciones entre Java Constraint Solver (Timefold) y CPython

    • Los problemas de rendimiento de FFI surgen principalmente por el uso de proxies para la comunicación entre el lenguaje host y el lenguaje externo
    • Las llamadas FFI directas usando JNI o la nueva interfaz foreign son rápidas, con una velocidad similar a llamar métodos de Java directamente
    • Sin embargo, los recolectores de basura de CPython y Java no encajan bien entre sí, así que se necesitan técnicas especiales para sincronizarlos
    • Si usas proxies como JPype o GraalPy, hay overhead de rendimiento, ya que hay que convertir parámetros y valores de retorno, y puede haber llamadas FFI adicionales
    • Si pasas objetos de CPython a Java, Java tiene un proxy para esos objetos de CPython
    • Si ese proxy se vuelve a pasar a CPython, se crea un proxy del proxy
    • Como resultado, el proxy de JPype es 1402% más lento que llamar CPython directamente por FFI, y el proxy de GraalPy es 453% más lento
    • Al final, convertimos bytecode de CPython a bytecode de Java y generamos estructuras de datos de Java correspondientes a las clases de CPython usadas
    • Como resultado, obtuvimos una mejora de rendimiento 100 veces mayor que usando proxies
    • Convertir o leer bytecode de CPython es muy inestable, está poco documentado y, por varias rarezas de la VM, es difícil mapearlo directamente a otro bytecode
    • Para más detalles, se puede consultar la publicación del blog: enlace
  • Gracias al blog de Rails At Scale y al de byroot, este es un buen momento para interesarse en discusiones profundas sobre el interior de Ruby y su rendimiento

    • Gracias a las mejoras recientes en Ruby y Rails, es un buen momento para ser Rubyist
  • Pregunta sobre si se puede compilar código con JIT en lugar de llamar una librería de terceros para llamadas a funciones externas

    • Estoy seguro de que ese es el principio básico del FFI de LuaJIT: enlace
    • Creo que esa es la razón por la que el FFI de LuaJIT es tan rápido
  • Información sobre una librería que usa JVMCI para generar código arm64/amd64 al vuelo y llamar librerías nativas sin JNI: enlace

  • Opinión de que "escribe tanto Ruby como sea posible, especialmente porque YJIT puede optimizar código Ruby, pero no código C"

    • Duda sobre si Ruby no es un lenguaje bastante lento
    • Si vas a pasar a nativo, querrías hacer tanto trabajo como sea posible en nativo
  • He usado Ruby por más de 10 años y es muy interesante ver los avances recientes

    • Qué emoción
  • Duda sobre por qué se necesita compilación JIT

    • La idea de que, si puede escribirse en C, quizá podría compilarse en el momento de la carga
  • FFI - Foreign Function Interface, es decir, una forma de llamar C desde Ruby

  • Pregunta de si eso no es justamente lo que hace libffi

  • Creo que ya entiendo por qué no fueron a tenderlovemaking.com