Skip to content

The Alternative Implementation Problem

April 20, 2024

In this post, I want to talk about a dynamic that I’ve seen play itself over and over again in the software world. In fact, I would venture a guess that this kind of situation probably happens in the hardware world as well, but I’ll speak about software systems since this is where my experience lies. This discussion is going to touch a bit on human psychology, and outline a common trap so that you can hopefully avoid getting stuck in it.

Most of my career, both in academia and industry, has been spent trying to optimize dynamically-typed programming languages. During my master’s I worked on a simple optimizing JIT for MATLAB. For my PhD I worked on a JIT for JavaScript. Today I’m working on YJIT, an optimizing JIT for Ruby which has now been upstreamed into CRuby.

During my PhD, while working on my own JavaScript JIT, I read many papers and blog posts about JIT compilers for other dynamic languages. I read about the design of HotSpot, Self, LuaJIT, PyPy, TruffleJS, V8, SpiderMonkey, and JavaScriptCore among others. I also had the chance to interact with and meet face to face with many of the really smart people behind these projects.

One of the things that struck me is that the PyPy project was kind of stuck in a weird place. They had developed an advanced JIT compiler for Python which could produce great speedups over CPython. By all accounts many people could benefit from these performance gains, but PyPy was seeing very little use in the “real world”. One of the challenges that they faced is that Python is a moving target. New versions of CPython come out regularly, always adding many new features, and PyPy struggles to keep up, is always several Python versions behind. If you want you Python software to be PyPy-compatible, you’re much more limited in terms of which Python features you use, and most Python programmers don’t want to have to think about that.

Reading about LuaJIT, I found that it was and still is highly regarded. Many people regard its creator, Mike Pall, as an incredible programmer. LuaJIT offers great performance gains over the default, interpreted Lua implementation, and has seen some decent adoption in the wild. However, I again saw that there are a number of Lua users who do not want to use LuaJIT because the Lua language keeps adding new features and LuaJIT is several versions behind. This is a bit strange considering that Lua is a language that is known for its minimalism. It seems like they could have made an effort to slow down the addition of new features and/or coordinate with Mike Pall, but this wasn’t done.

Almost 4 years ago, I joined Shopify to work on Ruby. For some reason, the space of Ruby JITs has been particularly competitive, and there had been a number of projects to build Ruby JITs. The TruffleRuby JIT boasted the most impressive performance numbers, but again, had seen limited deployments. There were some practical reasons for this, the warm up time of TruffleRuby is much longer than that of CRuby, but I also saw a similar dynamic to that of PyPy and LuaJIT, where CRuby kept adding features, and TruffleRuby contributors had to work hard to try and keep up. It didn’t really matter if TruffleRuby could be quite a bit faster, because Ruby users would always view CRuby as the canonical implementation, and anything that wasn’t fully compatible wasn’t seen as worthy of consideration.

Hopefully, at this point, you see where I’m going with this. What I’ve concluded, based on experience, is that positioning your project as an alternative implementation of something is a losing proposition. It doesn’t matter how smart you are. It doesn’t matter how hard you work. The problem is, when you build an alternative implementation, you’ve made yourself subject to the whims of the canonical implementation. They have control over the direction of the project, and all you can do is try to keep up. In the case of JITted implementations of traditionally interpreted languages, there’s a bit of a weird dynamic, because it’s much faster to implement new features in an interpreter. The implementers of the canonical implementation may see you as competition they are trying to outrun. You may be stuck trying to ice skate uphill.

Almost 4 years ago, with support from Shopify, two dedicated colleagues and I started a project to build YJIT, yet another Ruby JIT. The difference is that we made the key choice to build YJIT not as an alternative implementation, but directly inside CRuby itself. This came with a number of design tradeoffs, but critically, YJIT could be 100% compatible with every CRuby feature from the start. YJIT is now the “official” Ruby JIT, and is deployed at Shopify, Discourse and GitHub among others. If you’ve visited github.com today, or any Shopify store, you’ve interacted with YJIT. We’ve had more success than any other Ruby JIT compiler so far, and compatibility has been key in achieving this.

You may read this and think that the key lesson of this post follows the old adage that “if you can’t beat them, join them”. In some ways, I suppose it does. What I want to say is that if you start a project to try and position yourself as an alternative but better implementation of something, you are likely to find yourself stuck in a spot where you’re always playing catch up and living in the shadow of the canonical implementation. The canonical project keeps evolving, and you have no choice but to follow along with limited decisional power over where your own project is headed. That’s no fun. You may have better luck trying to join up with the canonical implementation instead. However, that’s only part of the answer.

In the Ruby space, there is also Crystal, a Ruby-like language that is statically compiled with type inference. This language is intentionally not Ruby-compatible, it has chosen to diverge from Ruby, but has still seen limited success. I think this is interesting because it gives us a broader perspective. Rubyists don’t like Crystal because it’s almost-Ruby-but-not-quite. It looks like Ruby, syntactically, but has many subtle differences and is very much incompatible in practice. This just confuses people, it breaks their expectations. Crystal probably would have had better luck if it had never marketed itself as being similar to Ruby in the first place.

Peter Thiel has a saying that “competition is for losers”. His main point is that you shouldn’t put yourself in a position where you’re forced to compete if you don’t have to. My advice to younger programmers would be, if you’re thinking of creating your own programming language, for example, then don’t go trying to create a subset of Python, or something superficially very close to an existing language. Do your own thing. That way, you can evolve your system at your own pace and in your own direction, without being chained by expectations that your language should have to match the performance, feature set, or library ecosystem of another implementation.

I’ll finish with some caveats. What I said above applies when you have a situation where there is a canonical implementation of a language or system. It doesn’t apply in a space where you have open standards. For example, if you want to implement your own JSON parser, there is a clearly defined specification that is relatively small and doesn’t evolve very fast. This is very much something you can achieve. You also have a situation where there are multiple browser-based implementations of JavaScript. This is possible in part because there is an external standard body that governs the JS specification, and the people working on the JS standard understand that JIT-compiled implementations are critical for performance and guide the evolution of the language accordingly. They are not in the game of adding many new features as fast as possible.

2 Comments
  1. Mike S. permalink

    I think your post raises the larger issue that the industry prizes innovation over stability, and we all suffer as a result.

    Cryptography APIs, unfortunately, have no choice but evolve as flaws are discovered and fixed. 

    But aside from tremendous (and impressive) speed improvements, does Ruby 3.2 or newer truly have killer features over 2.6? Do Python users absolutely need the features in the latest CPython point release over Pypy?

    I’m not saying it’s practical to pick an older version and standardize on it. It often isn’t. I’m just lamenting the state of the industry.

  2. In a slightly different realm. In the 1990s I worked on the international version of Lotus 1-2-3 for Windows and though Lotus 1-2-3 was king on DOS, Excel had become, by then, the canonical spreadsheet on Windows. Every time a new version of Excel was released the 1-2-3 team had to add the same features to catch up in order to be able to import the Excel sheet in. For example, Excel could add a new dotted line type and immediately 1–2-3 would not be 100% compatible. Compatibility was sold as the top feature and many loyal customers were persuaded to give up Lotus Suite and move over to Microsoft. Once Microsoft had achieved dominance then there was a bit of a backlash against new features in Excel as customers were suffering from version incompatibilities within the multiple versions of Excel and other software that they had. It was obvious then, that once you were forced to play catchup to someone else the game was over.

    I believe that the financial functions in Excel were originally pitched to Lotus by an outside developer who had created them as a plugin. This was not taken up, so he pitched them to Microsoft who included the whole new set of functions in Excel. Lotus were now on the back foot and hired the same developer as a consultant to help them implement them in Lotus 1-2-3 for compatibility with Excel!

    As you say, playing catch-up is a mugs game.

Leave a comment