1. 26
  1.  

    1. 4

      The wording of the blog post confused me, because in my mind “FFI” (Foreign Function Interface) usually means “whatever the language provide to call C code”, so in particular the comparison between “the function written as a C extension” and “the function written using the FFI” is confusing as they sound like they are talking about the exact same thing.

      The author is talking specifically about a Ruby library called FFI, where people write Ruby code that describe C functions and then the library is able to call them. I would guess that it interprets foreign calls, in the sense that the FFI library (maybe?) inspects the Ruby-side data describing their interface on each call, to run appropriate datatype-conversion logic before and after the call. I suppose that JIT-ing is meant to remove this interpretation overhead – which is probably costly for very fast functions, but not that noticeable for longer-running C functions.

      Details about this would have helped me follow the blog post, and probably other people unfamiliar with the specific Ruby library called FFI.

      1. 1

        Replying to myself: I wonder why the author needs to generate assembly code for this. I would assume that it should be possible, on the first call to this function, to output C code (using the usual Ruby runtime libraries that people use for C extensions) to a file, call the C compiler to produce a dynamic library, dlopen the result, and then (on this first call and all future calls) just call the library code. This would probably get similar performance benefits and be much more portable.

        1. 2

          I would guess because that would require shipping a C compiler? Ruby FFI/C extensions are compiled before runtime; the only thing you need to ship to prod is your code, Ruby, and the build artifacts.

          1. 1

            This is essentially how MJIT worked.

            https://www.ruby-lang.org/en/news/2018/12/06/ruby-2-6-0-rc1-released/ https://github.com/vnmakarov/ruby/tree/rtl_mjit_branch#mjit-organization

            Ruby has since that evolved very fast on the JIT side, spawning YJIT, RJIT, now FJIT…

            I’m also not sure if the portability is needed here. Ruby is predominantly done on x86, at least at the scale where these optimisations matter.

            1. 3

              Apple Silicon exists and is quite popular for development

              1. 1

                You’re correct. I was referring to deployment systems (where the last bit of performance matters) and should have been clearer about that.

                1. 2

                  Even in production, ARM64 is getting more common these days, because of AWS Graviton and al.

                  But yes, x86_64 is still the overwhelming majority of production deployments.

                  1. 1

                    Yeah, that’s why I wrote “predominantly”. Also, for such a localised JIT, a second port to aarch64 is not that hard. You just won’t have an eBPF port falling out of your compiler (this is absurd for effect, I know this isn’t a reasonable thing).

              2. 2

                Note that the point here is not to JIT arbirary Ruby code, which is probably quite hard, but a “tiny JIT” to use compilation rather than interpretation for the FFI wrappers around external calls. (In fact it’s probably feasible, if a bit less convenient, to set things up to compile these wrappers ahead-of-time.)