From its very start, Crystal was designed to be fast. It follows the same principles as other fast languages such as C. The compiler can analyze the source code to know every variable's exact type and memory layout before execution. Then, it can produce a fast and optimized native executable without having to guess anything during runtime. This process is commonly known as ahead-of-time compilation.
Crystal's compiler is built upon LLVM, the same compiler infrastructure that powers Rust, Clang, and Apple's Swift. As a result, Crystal benefits from the same level of optimizations available to these languages, making it well suited for computationally intensive applications such as machine learning, image processing, or data crushing.
But not all applications are CPU-bound. Most of the time, there are other resources at stake, such as network communications or a local disk. Those are collectively known as I/O. Crystal has a concurrency model similar to Go's goroutines or Erlang's processes, where multiple operations can be performed behind an event loop without blocking the process or delegating too much work to the operating system. This model is ideal for applications such as web services or file manipulation tools.
Using an efficient language such as Crystal will help you reduce hardware costs and improve perceived responsiveness from your users. In addition, it means you can run smaller and fewer instances of your application to address the same processing volume.
Let's take a look at a simple implementation of the selection sort algorithm written in Crystal:
def selection_sort(arr)
# For each element index...
arr.each_index do |i|
# Find the smallest element after it
min = (i...arr.size).min_by { |j| arr[j] }
# Swap positions with the smallest element
arr[i], arr[min] = arr[min], arr[i]
end
end
# Produce a reversed list of 30k elements
list = (1..30000).to_a.reverse
# Sort it and then print its head and tail
selection_sort(list)
p list[0...10]
p list[-10..-1]
This example already shows some neat things about Crystal:
- First of all, it is relatively small. The main algorithm has a total of four lines.
- It's expressive. You can iterate over lists with specialized blocks or use ranges.
- There isn't a single type notation. Instead, the compiler deduces every type, including the method argument.
Surprisingly, this same code is also valid in Ruby. Taking advantage of that, if we take this file and run it as ruby selection_sort.cr
(note that Ruby doesn't care about file extensions), it will take about 30 seconds to finish. On the other hand, executing this program after it has been compiled with Crystal in optimized mode takes about 0.45 seconds, 60x less. Of course, this difference isn't the same for any program. It varies depending on what kind of workload you are dealing with. It's also important to note that Crystal takes time to analyze, compile, optionally optimize and produce a native executable.
The following graph shows a comparison of this selection sort algorithm written for a variety of languages. Here, you can see that Crystal competes near the top, losing to C and coming very close to Go. It is important to note that Crystal is a safe language: it has full exception handling support, it tracks bounds on arrays to avoid unsafe access, and it checks for overflow on integer math operations. C, on the other hand, is an unsafe language and won't check any of that. Having safety comes at a slight performance cost, but Crystal remains very competitive despite that:
Figure 1.2 – A comparison of a simple selection sort implementation among different languages
Note
Comparing different languages and runtimes in a synthetic benchmark such as this isn't representative of real-world performance. Proper performance comparisons require a problem more realistic than selection sort and a broad coding review from experts on each language. Still, different problems might have very different performance characteristics. So, consider benchmarking for your use case. As a reference for a comprehensive benchmark, consider looking into the TechEmpower Web Framework Benchmarks (https://www.techempower.com/benchmarks).
A web server comparison
Crystal isn't only great for doing computation on small cases but also performs well on larger applications such as web services. The language includes a rich standard library with a bit of everything, and you will learn about some of its components in Chapter 4, Exploring Crystal via Writing a Command-Line Interface. For example, you can build a simple HTTP server, such as this:
require "http/server"
server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world, got #{context
.request.path}!"
end
puts "Listening on http://127.0.0.1:8080"
server.listen(8080)
The first line, require "http/server"
, imports a dependency from the standard library, which becomes available as HTTP::Server
. It then creates the server with some code to handle each request and starts it on port 8080
. This is a simple example, so it has no routing.
Let's compare this against some other languages to see how well it performs. But, again, this isn't a complex real-world scenario, just a quick comparative benchmark:
Figure 1.3 – A comparison of the request per second rate of simple HTTP servers among different languages
Here we see that Crystal is well ahead of many other popular languages (very close to Rust and Go) while also being very high-level and developer-friendly to code. Many languages achieve performance by using low level code, but it doesn't have to cost expressiveness or expose abstractions. Crystal code is simple to read and evolve. The same trend happens in other kinds of applications as well, not only web servers or microbenchmarks.
Now, let's get hands-on with using Crystal.