Programming Crystal Book Club

I want to join in :joy:

What computer have you got Mafinar? Yours is much quicker than mine :rofl:

$ time crystal build fib-crystal.cr
crystal build fib-crystal.cr  0.93s user 0.34s system 135% cpu 0.940 total
$ time ./fib-crystal
701408732
./fib-crystal  4.66s user 0.04s system 96% cpu 4.840 total
$ time rustc fib-rust.rs           
rustc fib-rust.rs  0.24s user 0.07s system 121% cpu 0.252 total
$ time ./fib-rust
701408732                  
./fib-rust  3.66s user 0.02s system 95% cpu 3.879 total
time ruby fib-crystal.cr
701408732
ruby fib-crystal.cr  55.80s user 0.48s system 99% cpu 56.348 total

Not sure whether it makes much difference building with rustc and crystal build : /

3 Likes

Very much so, I wasn’t expecting that, lol. I need to catch back up on its development. ^.^;

Heh, it was a lot of work, I just finally had a few minutes free from the baby late at night and needed to delve into something technical, it was very appropriately timed. ^.^

Should look at OCaml! It’s well known as having one of the fastest (and most pluggable) optimizing compilers of any language. ^.^

Hmm… Ooo 4.12 is out, updating!

And now with this OCaml code:

let rec fib = function
| 0 -> 1
| 1 -> 1
| n -> (fib (n - 1)) + (fib (n - 2))

let () =
  let sum = ref 0 in
  let () = for n = 0 to 41 do
    sum := !sum + fib n
  done in
  print_endline (string_of_int !sum)

Let’s compile it in optimized mode from scratch (with timing to show how fast it compiles, really obvious with lots of files as even my fairly decently sized projects often don’t even take a second):

❯ time ocamlopt ./fib.ml -o fib
ocamlopt ./fib.ml -o fib  0.18s user 0.09s system 119% cpu 0.226 total

❯ time ./fib                   
701408732
./fib  2.55s user 0.00s system 99% cpu 2.556 total

So basically what I expected. Compiled super fast, was almost as fast as C code, not quite as fast as the Rust, faster than crystal by a good margin, and this is all while remembering that even optimized OCaml code still uses tagged types, so even with that overhead it’s still that fast. For note, the blazing fast OCaml compiler is also written in OCaml. ^.^

And now let’s do the go code, so with the code in that screenshot then compile and run it (go version 1.16.3):

❯ time go build .
go build .  0.35s user 0.29s system 157% cpu 0.408 total

❯ time ./go_fib 
701408732
./go_fib  2.98s user 0.01s system 100% cpu 2.985 total

Interesting, go both compiles slower than ocaml and runs slower than ocaml, I figured it would have beat ocaml, interesting…

/* relatable */

Lol.

The one I’m using seems quicker than yours as well, it’s a few years-old Ryzen 7 that I’m running these on.

Oh wait, you didn’t compile the rust and the crystal code in release mode, you built them in debug mode, both will be slower that way! ^.^;

Look at the commands I ran in my post to build them each in release mode.

If you call the base compilers it probably won’t be as simple as adding --release to them like you can when using a build system as adding --release to the build system causes each’s build system to add a whole host of arguments to the compilers to optimize the code. You really should use their respective build systems, it’s a lot easier.

3 Likes

Ah hah! I found out Crystal has operators that allow wrapping with no exception throwing, you prepend & to things like + and so forth, so I updated the source to:

def fib(n)
    return n if n <= 1
    fib(n &- 1) &+ fib(n &- 2)
end

sum = 0

(1..42).each do |i|
    sum &+= fib(i)
end

puts sum

And compile and run!

❯ time shards build --release
Dependencies are satisfied
Building: crystal_fib
shards build --release  13.34s user 0.29s system 98% cpu 13.797 total

❯ time ./bin/crystal_fib 
701408732
./bin/crystal_fib  4.10s user 0.01s system 99% cpu 4.126 total

Hmm, I ran it a half dozen times and the same time result every time to within 0.01s, so it did help, but not as much as I’d hoped…

Taking a look at the assembly:

76385   │     .type   "*fib<Int32>:Int32",@function
76386   │ "*fib<Int32>:Int32":
76387   │ .Lfunc_begin239:
76388   │     .loc    7 1 0
76389   │     .cfi_startproc
76390   │     pushq   %rbp
76391   │     .cfi_def_cfa_offset 16
76392   │     pushq   %rbx
76393   │     .cfi_def_cfa_offset 24
76394   │     pushq   %rax
76395   │     .cfi_def_cfa_offset 32
76396   │     .cfi_offset %rbx, -24
76397   │     .cfi_offset %rbp, -16
76398   │     movl    %edi, %ebx
76399   │ .Ltmp10165:
76400   │     .loc    7 2 5 prologue_end
76401   │     cmpl    $1, %edi
76402   │     jg  .LBB239_3
76403   │     movl    %ebx, %eax
76404   │     .loc    7 0 0 is_stmt 0
76405   │     addq    $8, %rsp
76406   │     .cfi_def_cfa_offset 24
76407   │     popq    %rbx
76408   │     .cfi_def_cfa_offset 16
76409   │     popq    %rbp
76410   │     .cfi_def_cfa_offset 8
76411   │     retq
76412   │ .LBB239_3:
76413   │     .cfi_def_cfa_offset 32
76414   │     .loc    7 2 5
76415   │     leal    -1(%rbx), %edi
76416   │     .loc    7 3 5 is_stmt 1
76417   │     callq   "*fib<Int32>:Int32"
76418   │     movl    %eax, %ebp
76419   │     addl    $-2, %ebx
76420   │     .loc    7 3 20 is_stmt 0
76421   │     movl    %ebx, %edi
76422   │     callq   "*fib<Int32>:Int32"
76423   │     addl    %ebp, %eax
76424   │     .loc    7 0 0
76425   │     addq    $8, %rsp
76426   │     .cfi_def_cfa_offset 24
76427   │     popq    %rbx
76428   │     .cfi_def_cfa_offset 16
76429   │     popq    %rbp
76430   │     .cfi_def_cfa_offset 8
76431   │     retq
76432   │ .Ltmp10166:
76433   │ .Lfunc_end239:
76434   │     .size   "*fib<Int32>:Int32", .Lfunc_end239-"*fib<Int32>:Int32"
76435   │     .cfi_endproc

Well the test and jumps and exception are gone now, but it’s still generating very poor code, very weird…

Well, at least it’s faster than before, even if not as fast as any other native compiled language I’ve tested yet (even ocaml oddly)… ^.^;

2 Likes

Thanks ODL - that’s quite a bit faster!

$ time shards build --release
Dependencies are satisfied
Building: crystal_fib
shards build --release  0.83s user 0.29s system 119% cpu 0.936 total

$ time ./bin/crystal_fib 
701408732
./bin/crystal_fib  2.62s user 0.01s system 92% cpu 2.840 total

Your code from here is actually a bit slower :confused:

$ time shards build --release
Dependencies are satisfied
Building: crystal_fib
shards build --release  9.01s user 0.39s system 102% cpu 9.199 total

$ time ./bin/crystal_fib     
701408732
./bin/crystal_fib  2.68s user 0.02s system 93% cpu 2.901 total

That makes no sense, it remove the exception check, so it’s less code, especially removing a branch, which is costly… o.O

Seems to imply to me something else is a bit off…

1 Like

Maybe we need to put up instructions on how to get as close to your set-up as possible?

I installed Crystal using ASDF then ran:

crystal init app crystal_fib
cd crystal_fib

Then put:

  def fib(n)
      return n if n <= 1
      fib(n - 1) + fib(n - 2)
  end

  sum = 0

  (1..42).each do |i|
      sum += fib(i)
  end

  puts sum

At the bottom of the src/crystal_fib.cr file, then ran:

time shards build --release
time ./bin/crystal_fib 

Then swapped that code for yours:

def fib(n)
    return n if n <= 1
    fib(n &- 1) &+ fib(n &- 2)
end

sum = 0

(1..42).each do |i|
    sum &+= fib(i)
end

puts sum

Then built and run again - does that seem right to you?


Shall we split these posts into another thread @mafinar? I’ll leave it up to you :smiley:

1 Like

Either’s fine. This thread has been more of a learning experience for me than I had anticipated <3

Regarding the code, how I did this was just copied the content into fib.cr and ran crystal --release fib.cr and then./fib.

For me compilation times for both were more or less similar.

2 Likes


If you are splitting it, do it from the point where @mafinar showed his first benchmark.

3 Likes

One side-effect from this book club was, lots of motivation to learn more on compilation performance and benchmark.

3 Likes

I got the DS&A book in my basket since a while, so you said it worth the time to read it ?
It’s very cool to see that Crystal get traction.

We’re planning to make a POC at work, replacing Go by Crystal, since main of our services are written in Ruby. I’m gonna follow this thread very closely. Thanks.

3 Likes

I would say so. I mean, it’s a PragProg style algorithm book that has more pragmatism and less theory (and doesn’t cost 500 dollars). I’ve only skimmed through the chapters on Trie and Graphs, and found them nicely written. I will probably skip the first 5 chapters (Big-Os and market pitches) because I’ve been through those numerous other times.

There’s this other book from Manning, Advanced Algorithms and Data Structure, that treats more advanced type of algorithms like bloom filters, treaps, some ML ones, if you’re looking for less common algorithms and data structures. But as I am learning Crystal right now and would like to implement some algorithms with it, the “A Common Sense Guide” would cost less time for me. If I like Crystal enough to stick to it for a few more months, I’ll start the other book or maybe one of Manning’s “Classic Computer Science Problems in X” where X is Python, Java, Swift.

It’ll be great to know of your experience. As a language I certainly enjoyed Crystal more than Go, it’s been only 5 days and still I’m sold as far as syntax is concerned. Go has more libraries available, it will be interesting to hear about your thoughts there. I had similar concerns four years ago when trying Elixir/Phoenix but that never bothered me (too much) in practice.

Thank YOU! I look forward to hear about your journey with Crystal.

3 Likes

Seems about what I did yep, that --release on the shards build call is the thing that makes an optimized build, ditto with cargo build --release. :slight_smile:

Convenient! So the crystal compiler binary also takes --release as a shortcut, very nice!

Every book club I’ve been in gets very ‘offtopic’ in that in moves from topic to topic as things are learned and you can build on that, lol.

That’s cool, what I’ve learned is try to use type annotations in your code to help it compile faster. ^.^

It would be really cool to post a blog (or a post here or whatever) of how the conversion results go and all. :slight_smile:

2 Likes

Thanks for the advice! So the compilateur will avoid to make type inference based on the annotations I guess.

Sure, I’ll try to keep notes from the transition and if the POC was conclusive or not.

2 Likes

Love reading about both successes and failures, and the ‘why’ for each. :slight_smile:

3 Likes

Chapter 3 - Typing Variables and Controlling the Flow

Hello again! Now that I am off the post vaccine unpleasantries, I will put in my thoughts on Chapter 3, I skimmed it once, will get down with the code after brunch. It mostly deals with types, I got to meet a new type of method name, to_i*, to_f, to_a etc. The way to encourage types on number is similar, 25_i8, also, we have ? suffix to nullity management.

In the middle there’s a discussion on exception handling. So two things I usually check for when talking about exceptions: 1. How do you do an “else if” and 2. What are the exception handling syntax. So for 1. It’s elsif (Not elif or else if) and for 2. It’s begin..rescue...else...ensure and raise to throw.

An interesting discovery:

x = 10

ten_1 = if x == 10 "It's TEN" else false end # Works fine => Bool | String
ten_2 = if x == 10 1 else false end # Works fine => Bool | Int32
ten_3 = if x == 10 true else false end # ERROR: Unexpected token: `true`
ten_ternary = x == 10 ? true : false # I know it's redundant, but the syntax works as expected

Maybe I am missing something here. Another thing, I really really miss a REPL. Maybe it’s just me.

Another thing, if x == 10 10 else false end works, putting the condition and expression in the same line, but in case of case, case x when 10 10 else false does not work, it should be case x ; when 10 ; 10 ; false end. I just wish ; being relevant for if too could have been more consistent?

Also, the book was written in a time when there was case...when in Crystal. We now have case..in that is exhaustive. Also, the case does some than the C/Java ones do, for instance, you can destruct tuples, and call bool methods (i.e. when { .odd?, .even? } => ) and does type check. Not sure I mentioned this in an earlier post because I recall the book introducing case at an earlier section too.

Here’s a nice example from the book:

(1..100).each do |i|
  case {i % 3, i % 5}
  when {0, 0}
    puts "FizzBuzz"
  when {0, _}
    puts "Fizz"
  when {_, 0}
    puts "Buzz"
  else
    puts i
  end
end

One minor note, I found the name responds_to? method really cute, quite in line with how the original OOP used to be about messages and passing them around.

Middle of the chapter contained an example that glued all the knowledge together, and after that there was an introduction to composite data types. There’s Hash key => value and named tuple name: value, that was nice. There’s a type for Set, unlike Go.

So once you know the control flow, assignments and typings of a language, your knowledge of that language is somewhat Turing complete, you can do anything with it (though it’d not be pleasant). This chapter gets you there about Crystal, preparing you for the next step: organization. This really expands upon the first 50% of the previous chapter.

As usual, there was a Company’s story through an interview at the end. This time it is Dev Demand which mostly praised the performance and intuitiveness. There was tad comparison with PHP, Python and Go. To be honest, I think Crystal is intuitive for Ruby devs, just as Go is intuitive for people only habituated with C-Family members and Elixir for anyone who got habituated with patterns. For me, Crystal isn’t intuitive, but I’m sure Ruby will become once I learn it AFTER (if ever) Crystal

1 Like

Here’s an interesting snippet, something I found when solving Advent of Code year 2016, Day 1 problem. I created a dumb case to explain here.

def maybe_a_tuple(val : Int)
  if val.odd?
    {0, 0}
  else
    nil
  end
end

tuple = maybe_a_tuple(11)

# This throws an error, sure, maybe_a_tuple CAN return a Nil, so it's okay
# pp tuple[0], tuple[1]

# This works, because `if` ensures it ain't `nil`
#if tuple
#  pp tuple[0], tuple[1]
# end

case tuple
when {0, 0}
	pp tuple[0], tuple[1] # Compile Time Error
        # However, this is the clause that wins though.
when Nil
	pp "NIL"
else
	pp "ELSE"
end

So, if works to cast the Tuple | Nil into Tuple, but case didn’t, despite the clause {0, 0} winning which guarantees, there’d be a [] method somewhere. This may make sense once I dig in, but it certainly defied Principle of Least Surprise to me.

2 Likes

Woot!

I’m curious why they went with a _ separator, normally that’s a digit separater in programming languages and the language usually just does something like 25i8 or so by default.

Oh the horror, lol.

Very interesting how it doesn’t have something to separate the conditional to the ‘then’ expression, I wonder how much lookahead its parser performs and how much it affects parsing performance…

It has no repl? That’s pretty common now even for hard compiled languages (like rust has excvr if I’m typing that right, lol), because the jupyter ecosystem uses them heavily, checking… found:

It doesn’t seem to work standalone but it does seem to work in jupyter itself (jupyter interface rather than CLI), so perhaps that?

Ah, yeah that is a weird inconsistency…

That is also exceptionally odd! o.O

3 Likes

I am going to do a double chapter review tomorrow- chapters 4 and 5 aka methods and classes. It was nice read. I got to learn about yield and block aka that_thing_you_told_me_to_do_i_am_doing_with_these_parameters_or_not syntax. Loved it.

More in depth opinions on this coming up tomorrow!

2 Likes

Lol!

Looking forward!

2 Likes

Seems like this is a bug. None should compile and should have semicolon. There is an example of this in the book (I think Page 64), so I’d say not to follow this example. I had posted a question on Crystal forum last night on this.

2 Likes