Hacker News new | past | comments | ask | show | jobs | submit login
Ruby vs. Crystal Performance (ptimofeev.com)
175 points by ptcodes on June 5, 2020 | hide | past | favorite | 148 comments

Crystal and Ruby do not even remotely have the same semantics. They look similar but the similarity is extremely superficial. The extra semantics that Ruby has have a massive impact on performance, for even the most optimising implementations. This is not a reasonable comparison if you do not consider the 'slick as Ruby' part. No runtime metaprogramming! The entire Ruby ecosystem is built on runtime metaprogramming!

(Crystal is a fine language and compiler - but it's nothing to do with Ruby.)

Care to explain more? Having been using Ruby for about 8 years and Crystal for about 4, they actually have an extremely similar syntax and are also semantically very close. To the point where many Ruby scripts are completely valid Crystal, or at the very least require only a few changes.

I do think that people trying to compare Crystal to Ruby kind of miss the point though. Ruby as an interpreted language, even optimized with JIT compilation, will never match the performance you can get out of a true compiled language. By the same token, Crystal as a compiled language will never be as quick to develop with since you have to wait for your code to compile after each change.

> Care to explain more? Having been using Ruby for about 8 years and Crystal for about 4, they actually have an extremely similar syntax and are also semantically very close. To the point where many Ruby scripts are completely valid Crystal, or at the very least require only a few changes.

It doesn't have Kernel#eval. It doesn't have Kernel#send. It doesn't have Kernel#binding. It doesn't has Proc#binding. It doesn't have Kernel#instance_variable_get/set. It doesn't have Binding#local_variable_get/set. It doesn't have BasicObject#method_missing. It doesn't have BasicObject#instance_eval. I could go on. All these methods have extreme far reaching non-local implications on the semantic model and practical performance, and specifically defeat many conventional optimisations.

> To the point where many Ruby scripts are completely valid Crystal, or at the very least require only a few changes.

You can't even load most of the Ruby standard library without these methods!

And it doesn't matter if you use them or not. They're still there and they impact semantics and performance because the fact that you can use them affects performance. You can't even speculate against most of them as they're so non-local.

Rails and the rest of the mainstream Ruby ecosystem fundamentally depend on them.

> and are also semantically very close

Sorry I super disagree with this. They look similar. Dig into it just below the surface? Start to model it formally? Not at all. Method dispatch, which is everything in Ruby, isn't even close.

(Again, Crystal's great as its own thing, it's just not similar to Ruby's semantics. If you don't need Ruby's semantics or you can replicate them at compile time then maybe it's perfect for you.)

>> and are also semantically very close

>Sorry I super disagree with this. They look similar. Dig into it just below the surface? Start to model it formally? Not at all.

I think you're missing the point. If 90% of Ruby code works in Crystal unmodified (even if it's because the standard library had to be rewritten from scratch), then the programmer experience may well be quite similar, regardless of how fundamentally different they are if you model them formally.

Are Newtonian mechanics and Einstein's theory of general relativity "similar"? If you model them formally, they look nothing alike. But in 99% of practical situations in every day life, and even in the most precise experiments we could conduct for hundreds of years, they're so similar we can't tell the difference.

> If 90% of Ruby code works in Crystal unmodified

False, premise, since it's not the case at first place. 90% of your Ruby code will absolutely not work in Crystal unmodified.

That was an example, I was trying to offer chrisseaton a different notion of "similarity".

Another example: if 0% of Ruby code works in Crystal unmodified, but for 90% of code the transformation was extremely simple and mechanical like using curly braces {...} instead of begin...end and prepending $ to all variable names like Bash and PHP, they would still feel extremely similar in practice, albeit obviously less similar than the above example.

By contrast, Java and JavaScript are widely described as having very similar syntax, but it is rare to translate code from one to the other without require fundamental rethinking, because the relationship between JS objects, functions, and prototypes is so different from between Java objects, methods, and classes.

Depends on how you count. On a application level, no. On a class level, also no. On a method level, no but we are getting close. On a row level, possibly. On a token level, definitely.

> 90% of your Ruby code will absolutely not work in Crystal unmodified.

Don't nail me on exact 90%, but for me it does. Nothing rails related though. Good example: https://news.ycombinator.com/item?id=23437035

However, I agree that the fundamentals/underlyings are very different. It's far from being like a python 2 to 3 migration.

Agreed. Crystal looks like it has many positive characteristics, but having similar syntax has nothing to do with having similar semantics. Without constructs like missing_method, you cannot run practically any of the Ruby ecosystem libraries, including everything involving Rails.

Java and C also share a similar syntax, but that does not make that you can easily swap one for the other.

To be fair, method_missing has caused more nightmares and problems with debugging than probably any other feature in Ruby. I actively avoid using it, and even the Rails team has massively dialed back on its use in their libraries over the years...

I find this quite amusing because method_missing? has always been the difference between 'true' OO languages Smalltalk/Ruby and pseudo-OO language like C++; with the implication that true OO is better than pseudo OO for the ones making such distinction..

There has never been a "true" OO language. And if it there was, Smalltalk was not it. Alan Kay did coin the term, but Simula existed long before Smalltalk. The tree of languages that include C++, Java, and C# can be traced back to Simula while Smalltalk inspired Ruby. There is a distinct camp of "statically typed OO" (Simula and its children) and "dynamically typed OO" (Smalltalk and its children).

Yet none of this is the one true OO. All of it remains a way of describing a human mode of expression, and so is rightly subjective.


Programming Paradigms and Beyond, Shriram Krishnamurthi and Kathi Fisler:

OO is a widely-used term chock-full of ambiguity. At its foundation, OO depends on objects, which are values that combine data and procedures. The data are usually hidden (“encapsulated”) from the outside world and accessible only to those procedures. These procedures have one special argument, whose hidden data they can access, and are hence called methods, which are invoked through dynamic dispatch. This muchseems to be common to all OO languages, but beyond this they differ widely:

* Most OO languages have one distinguished object that methods depend on, but some instead have multimethods, which can dispatch on many objects at a time.

* Some OO languages have a notion of a class, which is a template for making objects. In these languages, it is vital for programmers to understand the class-object distinction, and many students struggle with it (Eckerdal & Thune, 2005). However, many languages considered OO do nothave classes. The presence or absence of classes leads to very different programming patterns.

* Most OO languages have a notion of inheritance, wherein an object can refer to some other entity to provide default behavior. However, there are huge variationsin inheritance: is the other entity a class or another (prototypical) object? Can it refer to only one entity (single-inheritance) or to many (multiple-inheritance), and if the latter, how are ambiguities resolved? Is what it refers to fixed or can it change as the program runs?

* Some OO languages have types, and the role of types in determining program behavior can be subtle and can vary quite a bit across languages.

* Even though many OO aficionados take it as a given that objects should be built atop imperative state, it is not clear that one of the creators of OO, Alan Kay, intended that: “the small scale [motivation for OOP] was to find a more flexible version of assignment, and then to try to eliminate it altogether”; “[g]enerally, we don’t want the programmer to be messing around with state” (Kay, 1993).

In general, all these variations in behavior tend to get grouped together as OO, even though they lead to significantly different language designs and corresponding behaviors, and are not even exclusive to it (e.g., functional closures also encapsulate data). Thus, a phrase like “objects-first” (sec. 6.1)can in principle mean dozens of wildly different curricular structures, though in practice it seems to refers to curricula built around objects as found in Java.

FWIW, Crystal does have compile-time method_missing. Which obviously is less powerful than the runtime variant, but it is still possible to get fairly far in many practical usages.

To be clear, crystal does have method_missing.

Crystal’s has something with the same name but like almost everything it has completely different semantics. You can’t use it for the same things.

To dig into method_missing a bit more: when you call a non-existent ruby method on any object it has to check for and run a method called method_missing, which can contain arbitrarily complex code, on the object itself as well as every class in the inheritance hierarchy. Because ruby is a dynamic language with dynamic dispatch, you can't easily precompute the results of doing this.

I programmed a little in Ruby and IMO all this dynamic stuff is redundant(to be polite).

Yeah how fast is that compiler? If it's just another compiled language (rather than the kind of wicked fast compiled language like Go), my enthusiasm will be dampened...

If it has reasonable incremental compilation, it can take a few seconds to compile.

With good code structure, I see large Java projects compile small changes in seconds, even though compiling Java used to be a hog. You don't often rebuild from scratch during development, do you?

I often switch between feature branches, when working on more than one project in a repo with multiple related modules. If there's a change near the top of that dependency graph, I'm forced to not exactly rebuild from scratch, but still to rebuild quite a lot.

Right now I'm at the very same situation. Yes, it is frustrating. This is why things close to the top of the dependency graph should be small, well-tested, and rarely need changes. But when you still need to troubleshoot them, there's no way around recompiling a lot of stuff if you want these static guarantees :(

Bad language design, IMO. The language shouldn't make the writers in it worry about how to organize the code to speed up the compiler.

In my case it's not even a language proper; I was using JavaScript which gives you near-zero static guarantees.

I was fixing an issue in one common library; properly testing changes required rebuilding and restarting a number of containers. Unit tests only tell you so much; you need proper integration tests to see how certain things interact.

If I were used a statically typechecked language (e.g. TypeScript), I could have eliminated 50%, or maybe 75% of the testing, because the compiler would check things for me before runtime. It would be drastically faster to localize and fix the bug even if the compilation increased build times 10x.

Often I do because that's what CI does. This is pretty normal.

But the point is having a different view of what compilation means in the developer's workflow as a language designer. Having the engineer have to think about how to organize the code for the compiler is bad design unless that organization is built into the compiler. The compiler should reject programs that are not organized for optimal compilation. And the organization required at least does not impede understanding of the code (best if it improves it). This is Go's design imprimatur and it's critically important to the success of Go.

FWIW, I see large Java projects compile small changes take minutes to compile, even using hot-reload tools.

Figwheel in Clojure is not like this, however: they're doing something right there.

However you frame it, compile times are going to be longer the more static guarantees you need to check, and the longer the more dependencies a particular code change affects.

Making your code low-coupling if equally beneficial for the compiler and for the human to reason about the code. Hence modularization, limiting the visibility of parts, etc.

OTOH there are situations when you have to have a common interface which is used across the board. Imagine Java's `List` or `CharSequence`. If you touch it, you have to recompile all the innumerable uses of it. So the more pervasive the dependency is, the smaller and simpler and more fine-grained it should be. Java's `List` does not do a hugely good job in the compactness department; it's pretty stable, though. You want the same trait from your most foundational interfaces.

I agree that the comparison is unfair - but I think the larger point is that there are many simple bits of Ruby that can copy/paste to Crystal with an immediate performance boost. In fact, it'd be interesting to slowly re-write a Ruby codebase into Crystal.

Of course, harder than it sounds, lots of specifics to figure out.

You are absolutely right. I have written Scheme interpreters in both languages. Compare https://github.com/nukata/little-scheme-in-ruby/blob/v0.3.0/...

  # Cons cell
  class Cell
    include Enumerable
    attr_reader :car
    attr_accessor :cdr

    def initialize(car, cdr)
      @car = car
      @cdr = cdr

    # Yield car, cadr, caddr and so on, à la for-each in Scheme.
    def each
      j = self
        yield j.car
        j = j.cdr
      end while Cell === j
      j.nil? or raise ImproperListException, j
  end # Cell
and https://github.com/nukata/little-scheme-in-crystal/blob/v0.2...

  # Cons cell
  class Cell < Obj
    include Enumerable(Val)

    getter car : Val            # Head part of the cell
    property cdr : Val          # Tail part of the cell

    def initialize(@car : Val, @cdr : Val)

    # Yield car, cadr, caddr and so on, à la for-each in Scheme.
    def each
      j = self
      loop {
        yield j.as(Cell).car
        j = j.as(Cell).cdr
        break unless Cell === j
      raise ImproperListException.new(j) unless j.nil?
  end # Cell
and they will make the point clear. Ruby and Crystal are different languages, but you can translate your code from Ruby to Crystal line by line fairly easily.

For the performance boost, see https://github.com/nukata/little-scheme/tree/v1.3.0#performa... which shows times to solve 6-Queens on a meta-circular Scheme as follows:

* Crystal 0.34.0: crystal build --release scm.cr: 2.15 sec.

* Crystal 0.34.0: crystal scm.cr: 9.88 sec.

* Ruby 2.3.7: ruby scm.rb: 84.80 sec.

Compiled (and complex enough) Crystal code runs 39 times faster than the equivalent Ruby code in this case.

Thank you for the relevant example!

Metaprogramming doesn't need to have a performance impact. VM languages like Java, C#, and JS allow you do define and modify code at runtime

JS you can redefine anything, Java support is pretty good, C# better support is coming with generators

Metaprogramming definitely has a cost, but it's one that you can minimise if you have the ability to either invalidate and recompile code at run time, or if you can perform extensive whole program analysis when ahead of time compiling.

The more extensive the meta programming you can do, the more work it is to implement this under the scenes. For example in Java you can change the visibility of fields, and you can load new classes, so that's not too hard to take account of, but in Ruby you can redefine methods, add refinements so they behave differently depending on where they are called, or radically change the inheritance hierarchy. Implementations like TruffleRuby can maintain high performance even with these features being used, but it's taken a lot of work to achieve that.

On top of Ruby metaprogramming being unique, V8 and Hotspot are amazing premium deluxe engines that have had more time and/or resources.

> Metaprogramming doesn't need to have a performance impact.

Optimising away the performance impact of most of the metaprogramming features I mentioned there requires truly heroic optimisations, beyond what has ever been used for any other language.

Some of them are even worse - I'm not sure there any way to optimise away the non-local effects of Proc#binding, which allows you to access local variables not lexically referenced.

Ruby's meta-programming capabilities is why optimising Ruby is so darn difficult. It's extremely powerful, but also complicates a lot of things.

I reason the difference is mostly when metaprogramming can happen. In Java, redefining or adding code is very explicit, it can't just happen whenever. Same with C#. And there are lots of rules. A class can't modify itself, and there's limits to what changes you can make. And to make changes you must have control over the "classloader" that loaded that code.

JS on the other hand can do most of what ruby does, to my knowledge. Objects are key value pairs so you're free to mess with them in virtually any way you please. You can also mess with the inheritance by altering JS prototype chains.

I don't think metaprogramming itself has much to do with the speed of Ruby, with my admittedly limited knowledge of this stuff

However, JS does not treat everything as an object as pervasively as Ruby does. A JS object and a JS integer are two different abstract data types. A Ruby number literal "1" is treated as an object of the Integer class. There are no separate abstract data type other than an object. All operators for an Integer can be overriden (in runtime), or perhaps, a specific object's methods can be overridden. Now granted, the runtime cheats and implements certain things in C ... but those can be overriden during runtime ...

If you want to find out more about the limits of what can be done to optimize Ruby, check out the Truffle project. That came out of someone's PhD dissertation on novel methods for doing JIT optimization for Ruby. It is sufficiently difficult and novel to warrant awarding a PhD for. Last time I heard, Truffle still could not run Rails.

A JS integer is actually an instance of the "Number" object, you can to a small degree alter fundamental behavior even with primitive types

No it is not. It is a primitive, this difference is well defined in the ECMAScript spec. Same for strings. An instanceof String is strictly not the same as a string primitive (and there are runtime consequences).

You need to enclose numbers to access the prototype: 1.toString is undefined, whereas (1).toString() is "1"

I stand corrected.

Ruby's "metaprogramming" is something Java, C# and JS don't do well -- dynamically redefining things during runtime. Everything in Ruby, including literals and operators, can be redefined during runtime, because everything is an object, and every message passed to any object can be redirected, filtered, transformed ad hoc. It's not just classes can be modified. Specific objects can be modified. Well-crafted Ruby code breaks things up into mixins that can be composed together. The closest comparison is one of Ruby's inspiration -- Smalltalk.

I think the most exciting optimization people have seen with Ruby is Truffle.

I don't regret the 14 years I put into writing Ruby professionally. I've used and abused metaprogramming, and it has shaped how I reason and architect things. I learned to appreciate well-designed, semantically-meaningful DSL. But I've moved on. I write server code with Elixir these days, and I'm exploring other ways of reasoning and writing code.

Why did you have to move on?

I didnt have to move on. I chose to. I had learned what I wanted from Ruby, and I started to realize that the problems I was facing was leading me to reimplement some of the things that OTP already offered. I was getting more interested in concurrent, resilient systems. It came about the time when it converged with my interest in permaculture (which is also about resilient, regenerative systems).

Java and C# essentially let you dynamically load/JIT code (not to be confused to the JIT-to-native virtual machine implementation they often run on); JavaScript is much closer to Ruby in that sense. It also gets really slow if you try to do any of those things extensively.

you obviously did not do metaprogramming in Ruby. Try it and you will never look at the "metaprogramming" capabilities of other languages in the same way.

no offence to JS, but JS and a proper programming language are not even the same species.

For real metaprogramming try Clojure. It's another world entirely.

Lisps are far more natural at metaprogramming; Clojure is the most most popular Lisp at the moment.

isn’t clojure like a lisp with extra steps?

Ruby has to have runtime metaprogramming because it has no other time but run time.

Compile-time metaprogramming is way safer and more performant, but for it you need a compiler.

But Crystal doesn't have those either.

This article by one of the Crystal developers explains why this comparison is not fair: https://crystal-lang.org/2016/07/15/fibonacci-benchmark.html.

> When doing operations between integers, Ruby will make sure to create a Bignum in case of overflow, to give a correct result.

> Now we can understand why Ruby is slower: it has to do this overflow check on every operation, preventing some optimizations. Crystal, on the other hand, can ask LLVM to optimize this code very well, sometimes even letting LLVM compute the result at compile time. However, Crystal might give incorrect results, while Ruby makes sure to always give the correct result.

Since late last year crystal made overflow checking the default (https://crystal-lang.org/2019/09/23/crystal-0.31.0-released....). Dunno how it changed the benchmark.

You may want to edit the link.

Whoops. Thank you!

Coming at Crystal from a Go and PHP development background, here are my thoughts:

- The syntax is lovely. No, really.

- I hate waiting for it to compile, especially compared to Go's compile time.

- It's really young yet, and the ecosystem is just getting started.

> - The syntax is lovely. No, really.

Matter of taste; I find it horrible, just as I find Ruby syntax horrible. And I do not care about syntax too much generally (among my production langs are k and clojure) but I find this an eye sore; don’t know why but it is what it is.

Edit: aaah downvotes for an opinion :) Anyway, background; I maintained a huge Rails codebase for years; it was pretty much the worst thing I ever did (in 30 years of production coding) and that was pretty much down because how much I don't like the syntax. That doesn't happen often.

For what it's worth, I thought your comment was reasonable given the discussion and upvoted it.

You're allowed to hate Crystal's syntax. I'd be curious to hear why, though.

Ruby syntax's alleged beauty is one one of its most popular features and you're in a thread about a language that intentionally copied that feature.

I don't like rails either but I like ruby syntax. You're sure you don't like ruby and not rails?

You got downvoted for not liking something "just because". (I didn't downvote.)

Is that not a matter of taste means? I even say I do not know why; I just break out in hives when I open a ruby file. When I open up a file to start programming, I have to have some feeling of ‘let’s do it’; I have that with most languages but with ruby I just think ‘crap this is ugly’. Not sure how to explain that; it is more like trying to explain why I prefer Klee over Rembrandt. I guess some people can but I cannot put that into words.

I share the same feeling when opening up rails project because of their own weird rules and files being all over the place. I don't feel that when writing a single ruby file and feel elegant.

I don't know...


I find almost every other implementation better on the eyes; picolisp, pari/gp, nim and some others I would call elegant; the Ruby version looks like overly noisy and fairly ugly to me. Not so much (but still) the function defs, but p (Array.new(20) {|n| F(n) }) I find pretty nasty for some reason (again, not sure why).

Randomly picked example.

A massive point for me missing in this article is the experience of having to wait for Crystal to compile - especially for development, this sucks. Try timing puts "Hello, world" on both:

  ~ % cat test.rb 
  puts "Hello, world"
  ~ % time ruby test.rb
  ruby test.rb  0.03s user 0.05s system 25% cpu 0.312 total
  ~ % time crystal run test.rb
  Hello, world
  crystal run test.rb  1.48s user 1.09s system 72% cpu 3.559 total

I find this perspective kind of funny, because every medium-sized Ruby project I've worked on takes 10+ seconds just to load and start rspec (before it runs any of my tests) or a REPL.

Because of this, working with Ruby is actually substantially slower than working with a similarly sized Go project, in my experience, even though Go theoretically has the disadvantage of needing to compile things before running them.

Plus, Go's type system will catch tons of bugs that Ruby optimistically treats as "maybe you meant to do this". Obviously Rust's type system is really awesome, but Go's is still really helpful, while compiling much faster than Rust.

Since Rust and Crystal are both LLVM-based, I would guess that Crystal's compile times also tend to be a bit painful on medium-sized projects, like Rust, but I've never actually used Crystal for anything more than "Hello, World".

(I've been paid to work full time with Ruby, Rust, and Go over the years, and they each have pros and cons, but I just don't think I would ever personally choose Ruby for any new project in 2020, if I had a say in it, given how great Go is for web development.)

I wonder if it would be possible to write in a common subset of ruby and crystal, and use the former for dev and the latter for prod?

The test was against compiled Crystal code:

crystal build program.cr


I'm excited to see Crystal grow - I think it has a lot to offer. I know it's not an intended consequence or a goal of Crystal's development, but you'd be surprised how easily plain Ruby maps to Crystal's syntax and method name choices. It can come in handy when you're looking for an easy performance boost.

Actually, I would argue that mapping Ruby's syntax and method name choices to Crystal has been a major goal in Crystal's development. It's practically in the tag line for the language, "Fast as C, Slick as Ruby"

The thing I like about Crystal is that I can generate statically-linked binaries and distribute them. I can quickly build utilities for people to use and don't have to distribute the runtime. The type system wasn't as bad as I thought (especially with type inferencing). Having written Ruby professionally for 14 years, Crystal was relatively easy to pick up. There are a many libraries that hew closely to their Ruby counterparts.

There are some amazing things I love about Ruby, like the metaprogramming and monkeypatching ... but I have moved on. I do my server work with Elixir these days.

Neat seeing crystal on here.

I built my startup using crystal and now I write crystal full time.

I’m still only using a single dedicated server with a SQLite database.

I'm really curious why you picked Crystal for your startup :) Currently I'm evaluating different stacks and so far Crystal and Elixir are the ones that stand out the most. Did you evaluate other languages and frameworks before settling on Crystal? What has been your biggest pain points so far with the language / ecosystem? Would love to hear more :)

Yeah i evaluated elixir, node.js, ruby, and ocaml.

Pain points with crystal have mostly been about one of the couple packages i depend on have a breaking change. It was common for crystal to deprecate an API which makes its way to a dependency. Its less common now in the last few months. Maybe I'm just not using the lock file correctly?

There is a little verbosity when parsing user input, like JSON or query parameters, but I've embraced writing bigger schemas or logic for validating input and its made the software better.

BUT, one thing that has been absolutely important was a static compiler. I remember using node or ruby and how many bugs i would catch later in prod after refactoring. Crystal helps big there, as would elixir since its functional but its not statically typed.

Elixir scales but I'm not a huge company yet. I want raw speed right now for the few customers i have. Elixir was a close second.

Crystal compiles slower than most languages, but (except for my website) I just use it as an API server and everything else is a Preact app so I spend most time on the Preact/Webpack stack.

Full time? Awesome! What is your start up?

Are you using a framework? Please tell us more :)

We are a delivery-only cookie company. We process orders online and have lots of bakers/drivers delivering thousands of cookies a week, locally. The backend system processing orders, managing orders records and customers (ERP/CRM), and assigning to drivers is all Crystal on the backend. We started almost 2 years ago and the coronavirus really validated our business model. We don't use any 3rd party delivery services like doordash so our drivers can do 4-6 orders per hour.

No frameworks. the only external crystal dependencies are: sqlite3, awscr-s3, and jwt.

I was using just crystal template views (ECR) on the frontend but its not maintainable or advanced enough so I'm slowly transitioning everything to Preact.


Why SQLite?

That’s how I started it. I didn’t want to start a company with a complex setup. The whole service runs on a single machine so it’s very fast and simple. I just back up the SQLite file every hour to s3. Its easy to download that file from s3 to use production data in development.

If I start getting scaling problems I’d have to start using multiple servers and at that point I’d need Postgres.

But we’re at ~36 employees and 8 figures of revenue on a single server.

I can probably go another year or 2 without a big database.

What are you trying to ask? Why not?

I sort of felt compelled to post this. An OS written in Crystal. [1] Which got quite a bit of discussion last time it was on HN [2] .

I know this may sounds strange but I would not be surprised once Crystal reached 1.0 there will be a Ruby implementation written in Crystal.

[1] https://github.com/ffwff/lilith [2] https://news.ycombinator.com/item?id=21860713

Some underrated live coding tutorials on Crystal on this YT channel: https://youtu.be/Q1wOFmt3yng

I knew who it was before clicking. I learnt to love Crystal from this channel. Great content.

The recursively written Fibonacci benchmark is terrible for comparing a interpreted language to a compiled language. When compiling with optimizations in C, for example, the compiler significantly transforms the program. [1] It's possible Crystal does too.

[1] https://godbolt.org/z/vaESBG

Recursive fibonacci is highly artificial, but the fact that a compiler can greatly transform the program is an important advantage of compilers over interpreters.

That's sort of true, in the abstract, but it wouldn't explain what's going on here. Even JavaScript (with a JIT, but not a separate compilation step, and certainly no type annotations) is faster at this than compiled Crystal. There's clearly other factors which are much more significant than simply having a compiler.

When you're doing something like writing your own routine for calculating fibonacci numbers, perhaps. But that's not the kind of optimization that's likely to be significant to most developers writing business applications.

I would also argue that the ubiquity of things like BLAS wrappers largely blurs these lines. For example, one of the big reasons I choose Python over Java (my company's primary language) for my work is that, thanks to numpy, Python absolutely smokes Java at crunching numbers, for my purposes. This despite Java being a compiled static language and Python being an interpreted dynamic language.

I had the (mistaken) impression that the GCC code signicantly transformed the algorithm because there is only one recursive call instruction, but looking at it closer it's still exponential. I still think it's a pretty silly idea to use such an unrealistic benchmark. Especially since it might be possible to make a compiler turn the exponential algorithm linear.

I'd be very surprised if there's a commercial compiler that would automatically optimize this into linear time. Maybe some obscure functional research language? But certainly don't think LLVM is capable of dynamic programming.

It can be an ok way to measure procedure call overhead though (between languages with similar semantics)

Maybe, but these two languages don't exactly have similar semantics.

You can't make a Ruby program that uses fixnums-only. You also can't make a Crystal program that's dynamic like Ruby. Interestingly, though, when the Crystal implementation is modified to use bignums, the Ruby implementation is over twice as fast while still being dynamic.

What I get from this is that Crystal's static compiler is slower than Ruby's dynamically dispatched interpreter, and Crystal's bignums are so slow you'll need to think hard about whether you want a 50x speed boost or correct arithmetic in all cases.

I think Crystal is a good concept, but it's only version 0.34.0. Every implementation is bad at version <<1.0. Ruby <<1.0 didn't have good performance, either. I'm sure Crystal will be great by the time it gets to 2.6.5, too.

I really liked Crystal some years ago, even used in a prototype. Soon I realized that despite its cute syntax and good performance, coming from Elm, I would prefer a simpler language with type safety, good error messages and very fast compile time. Maybe in the next years I'll experiment again, especially due to Lucky web framework.

May I ask what language you decided to move to instead?

In my current job I use Node/Koa with Js, and I am not happy with it. Every side project that I start I use Elixir, and I plan working with it next year, or maybe with Clojure. It's not exactly what I was looking for, but while I can't find one that matches the criteria in the next years, these are pleasant options for me.

Here are thorough benchmarks of Crystal and C up against other interpreted languages: https://github.com/kostya/jit-benchmarks. It has other examples besides Fibonacci.

I'm quite surprised to see Javascript / Node so high up the ranks, quite impressive the amount of optimizations that's been done to the engine throughout the years.

Yeah, a lot of people don't realise that JavaScript performance is closer to Java than Python, even though the semantics are much closer to Python.

We recently finished our first node backend (typescript) and I have to say it was an amazing experience. The npm ecosystem definitely attracts its fair share of undesirables, but I struggle to see why JS full stack isn’t the default for 99% of people.

Lots of people are sure their stack provides an amazing experience, for example I don't get why you would choose node over rails. As for the performance: looks good for js, but quite a memory hog or am I missing something?

I'm not expert in Ruby, but I'd say there's definitely a few wins in typescript (types, performance, async). But the biggest wins have nothing to do with language differences, but rather not having to worry about that because you're using one language. If teams are large and split between api and frontend, then perhaps less of an issue. But if you're doing anything fullstack, to be able to use one language, everywhere with mostly the same ecosystem...well it's kind of what Java wanted to be.

And I say this coming from a python/django background. And ORM is one big thing that Rails/Django definitely are clear leaders in.

I like the one language thing. I think it's overrated a bit (most syntax mistakes I make are caught by linters) but it's nice. However, it's still js. Lots of warts still. Also - for a normal web app I really don't see how async is a plus. I don't like promises or async that much, especially with so much db access - why would i want everything to go to an event loop?

It's a never ending discussion, I think all of these stacks are "good enough" to get the job done, a lot of it is how your brain ticks and what you're experienced with...

If all you are doing is writing API then you already don't need 50% of what Rails provides. If you are not into ORMs then you don't really need anything Rails provides at all.

Sure, if you don't need Rails than you don't need Rails I agree

Also same source, but more comparison against a wider variety of other languages and general benchmarks of data structures and algorithms: https://github.com/kostya/benchmarks

What’s the elevator pitch on Crystal? Seems like typed Ruby with performance considerations.

Crystal isn't really "typed Ruby" because that would mean it includes all of the same metaprogramming capabilities that Ruby does. Crystal is a lot more limiting than Ruby in this regard. For example, Crystal doesn't allow you to automatically generate class constants.

I would say the elevator pitch is "A systems programming language with an expressive syntax, similar to Ruby".

Exactly, it's basically statically typed, compiled Ruby with really good performance.

Also it has really powerful type inference, which allows you to write really Ruby-like code while maintaining your static typing. The downside of that is that it makes the compiler kind of slow.

Does go-to definition work in crystal? That was always my least favorite thing about ruby, I find it very awkward to write code without being able to jump to definitions.

This is the reason why I have to keep IntelliJ Idea Ultimate/RubyMine laying around.

I don't have the greatest laptop, and run a lot of containers, so Jetbrains IDE's are generally a no-go for me and I stick to VS Code, but trying to develop Ruby/Rails without a Jetbrains IDE Is crippling.

I think ctrl+click definition jump does work in Crystal and it has a decent language server.

That's good to keep in mind, I strictly write code in vim but if I have to pick up ruby again I'll give those IDEs a shot.

I really like the sales pitch of rails, but as someone that almost exclusively writes in modern, compiled languages, it's really tough to adjust to ruby and be productive with it.

I wrote Rails professionally for a few years back when that was THE de-facto tech for greenfield projects and new startups.

At the time, I really enjoyed it, but I'd never written a modern-feeling typed/gradually typed language yet (C#, which I found really verbose and ugly, a lot of pre-ES6 JS and Ruby, some Lua + Python).

I would never voluntarily write it again. Ruby/Rails completely falls apart in larger projects if the entire team doesn't stick to best practices + stringent code and doc standards with stuff like YARD or Sorbet. And so many magic, implicit methods injected everywhere.

Super happy writing Typescript and occasional Rust/Go now. I thought Kotlin was pretty nice too, the one small project I had to implement in it.

Another issue with writing rails is the "bug in production that would've been caught at compile" issue lots of people have. There's been multiple instances early in my career when I missed a nil guard somewhere that broke months later because someone else used the method and sent it a nil. Types are super useful because they also work as on-demand accessible documentation, and the compiler helps you figure out well in advance when you're sending an invalid input etc.

I mean, the answer is just, more tests. If you choose a dynamic language over a compiled one, then you've in essence chosen to eliminate type definitions in code and replace them with type verifications in tests. Of course, if you switch to a typed language that doesn't have nil types, that doesn't help...

Sorbet ? So you can't write maintainable dynamic code without basically making it static is what you're saying.

When you have +100,000 LOC written over many years by people with varying degrees of experience and familiarity with the language, Ruby/Rails becomes unmanageably difficult a lot faster than other languages. It isn't the only one though.

Not that you can't have a great, large Rails codebase but the "guardrails" are a lot less present than in some languages and it can be harder to try to clean up the mess once it's happened.

Hmmm well in the current fad of breaking out everything into a "micro" service how often do you deal with 100K LOCs? Also, typed code will reach 100K locs way faster than dynamic.

The crystal language client can attach to one of the vim language server implementations just fine and make the same functionality available

Huh, that seems a bit surprising to me. I did rails development for a year on sublime text and had a grand old time.

There are go-to definition plugins that work for Ruby, depending on what editor or IDE you're using. They're just not always perfect, because in a non statically typed language, you can't always know what the implementation of a method is.

Last I checked, there wasn't great editor support for Crystal yet, but that might have changed by now.

With solargraph I have great go-to-def when working with ruby in VSCode. It doesn't do well with jumping to def for gems/ dynamic methods, but a solid 80% of the time I can jump to/from references easily.

What is your editor of choice? Go to definition has worked for Ruby in Sublime for a long time. I actually can't remember a time it didn't work.

For a while, it was Ruby's ease of use with C's performance.

I believe that now they're trying to reduce marketing that relies on Ruby and present more as an independent language (which it is).

Fast as C, Slick as Ruby with their own implementation of Go's 'goroutines' called Fibers.

More here:


"Go" with Ruby like Syntax.

Except Crystal has real pointers and can be used in kernel-space.

It's probably closer to Rust with ruby syntax?

If `brew install crystal` is taking more than an hour to complete, in my case it was because llvm is a dependency and for some reason it was trying to compile it from source. If you manually install a precompiled llvm (you can do `brew install llvm` with some flags) it should skip that step during the crystal installation.

I installed crystal almost instantly with ASDF. Just:

    asdf plugin-add crystal https://github.com/asdf-community/asdf-crystal.git
    asdf global crystal 0.34.0 

It was painless. Other languages I've installed with ASDF must have had the same dependencies.

> If you recall Crystal is a statically typed language but you can omit explicit type restriction and the compiler will try to infer the type of variable. In our code Crystal uses the Int32 type for the n variable which has the maximum value of 2,147,483,647 but the 47th number is higher. In this case we need to specify the type of n. We can use Unsigned Int 64.

Both Ruby and Python use arbitrary-precision integers throughout. I thought Crystal would do the same, given the frequent comparisons to Ruby. I assume the reason it doesn't is to enable AOT compilation (i.e. not only because Crystal is statically-typed)?

Is there a statically-typed language that does use arbitrary-precision integers throughout?

More generally, what other features are common in dynamic languages and rare in static ones, that aren't directly related to type-safety?

Haskell has arbitrary precision.

JavaScript has fixed precision.

Well, they both have both. More or less. Haskell has Integer for arbitrary and int for fixed precision integers, for example. JavaScript has arbitrary precision integers (via BigInt), and fixed precision floats.

Crystal would benefit a lot if ported and packaged for ARM architectures. Those small embedded boards seem the best place to make use of a system language that produces tight and fast native executables. I know there's something for the Raspberry-PI+Raspbian, however I hoped there was an official port for other boards on a more generic distro such as Debian, Armbian or DietPI. Being a bootstrapped compiler (a compiler written in the same language it compiles) makes this even harder to accomplish.

Huge ruby ran here. Ruby on Rails has been my day job for a decade, and it pays the bills well enough. Crystal has always been a curiosity to me, but I have never been as productive in any other language or framework as I have been with Rails. I dont' care if you're language is "ruby-like" or "go fast" - if I'm not as productive with it as I am with Ruby, I'm not gonna touch it.

Get back to me when there is Crystal on Rails.

I'm sure that it's nowhere near as productive as Rails, but this framework is probably best positioned to be the Rails of Crystal, in case you haven't seen it:


Does it also have a massive community and thousands of libraries(rubygems) for almost every possible thing you can possibly think of?

It's a significantly younger language than Ruby, so no, the ecosystem is not as diverse (yet). It is quite populated though - for a taste you can browse at sites like https://crystalshards.xyz/ or https://shards.info/.

I think language-to-language comparisons are often "unfair". Comparing Ruby and Crystal is probably not much better than comparing Ruby to C. Syntax is not an absolute signal for language similarity, and the most important parts of Ruby (in my opinion) more or less require it to be interpreted and not compiled. One can always make languages look the same (which Facebook did with OCaml <-> JS transpiler) without the languages being similar by other measures.

tl;dr "yes, that one is faster and looks similar to the other one so our developers will be less scared and this is good" is important and valid but "that one is just the fast language but slower" is not a valid comparison, which I'm kind of getting.

I really like Crystal and have had good success using it for web stuffs. I still reach for ruby first, because the dev feedback loop is a lot faster and the ecosystem is more vibrant.

The biggest gain I've found in using Crystal vs Ruby is reduced memory usage.

Most often my code isn't slow because of the language, but the data access behind it, so counting CPU cycles is less of a priority for me.

Crystal is quite interesting, but a fib benchmark?

I mean it's kinda trivial but it does have the added benefit of also showing off that it's statically typed. For a more real world example check out the website of Kemal (Crystals Sinatra equivalent) at https://kemalcr.com/, where it benchmarks at ~46k requests per second in a HTTP benchmark vs 4k in Ruby.

Or something like Sidekiq in Crystal [1], which was 7 times faster. But now I wonder how much faster would Sidekiq run on TuffleRuby.

[1] https://github.com/mperham/sidekiq.cr

I'd be amazed if TruffleRuby could beat Crystal, given the problems most JITs have had with Ruby. By restricting the extreme dynamism of Ruby a bit, Crystal can allow for optimalisations that no Ruby implementation will be able to match. It's just really difficult to properly compile languages where it's possible to dynamically redefine the + operator based on input from HTTP requests. That said, Truffle is looking super cool and Crystal does not have anything near the size and quality of the Ruby ecosystem yet.

Here is a much more fair comparison: Ocaml vs Crystal vs Go:


I would like Crystal to include some modern features instead just being a fast and nice language. How about stuff like: immutability, good functional programming support, complex union types, compile-time guarantees?

Check out Crystal’s official website sometime to learn more about its modern features. Crystal doesn’t have immutability, but does have support for functional programming and union types. “Compile-time guarantees” is such a vaguely specified feature that virtually all programming languages have that, but its type system is actually pretty strong despite how it looks with all the type inference, and even prevents null pointer exceptions unlike a lot of static languages.

It also has a lightweight threading system that makes it work well as a web server. Not quite as elegant as something like Elixir, with its isolated processes, but still nice. It also supports macros, which can make the code a lot more elegant and performant compared to similar languages.

It’s cool if it looks like Ruby but if it doesn’t run Ruby gems, it’s kinda pointless to consider it a Ruby alternative. Might as well consider Rust or C++

some of these benchmarks Fibonacci etc are superficial. don't reflect real world use. people need to start comparing languages on ecosystems etc, error reporting e.g Elm | Rust have beautiful error messages. time to write a feature and deployment. Your language | framework could run in microseconds etc but if getting started is a nightmare or it has cryptic error messages, then it's non starter

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact