Interesting question about heap allocation vs stack allocation vs garbage collector

def test
  a_on_the_stack : Int32 = 2
  puts a_on_the_stack
end

The object a_on_the_stack of type Int32 is allocated on the stack, so there is no GC involved. When the stack is popped, the variable goes away.

If instead I wanted it to go to the heap, I would do:

def test
  a_on_the_heap = Int32.new(2)
  puts a_on_the_heap
end

Because a_on_the_heap goes out of scope after the method returns, I assume that the GC would eventually kick in to free the memory allocated on the heap by the variable a_on_the_heap above. Is that correct?

My question then becomes:

Is there any way in Crystal to free the allocated heap memory manually, without having to rely on the GC?

Something like:

def test
  a_on_the_heap = Int32.new(2)
  puts a_on_the_heap
  free a_on_the_heap # <=========== HERE
end

Thanks!

I’m pretty sure 2 and Int32.new(2) are both on stack as latter is essentially just doing 2.to_i32 and Int32 is a struct which is a value type.

You can manually call GC.collect. But, as far as I know, there isn’t a way to circumvent that for memory that was originally allocated by the GC? Or if you do, I imagine that would cause some issues if the GC tries to free memory that’s already freed, or maybe it would know that?

GC.collect doesn't seem to be working. · Issue #11491 · crystal-lang/crystal · GitHub and WeakRef doesn't seem to work? · Issue #10469 · crystal-lang/crystal · GitHub may also be worth a read.

2 Likes

Exactly. It seems @rkasinsky might be confused with Java semantics, where int is a primitive type and Integer is a class type. In Crystal, there is no such distinction. Int32 is a value type, so it’s located on the stack.

This was actually just asked on Stack Overflow, see answer there: How to turn off the GC in Crystal and do memory management yourself, without any GC? - Stack Overflow

3 Likes

So Int32 works as a Java primitive, in a sense that it never goes to the heap and because of that it never needs to be garbage collected. And it is also an object, so we get the best of both worlds.

I work with real-time systems, so any GC activity is unaccepted. It would be important for Crystal to allow manual memory management. This is paramount for low-latency and systems programming.

Now I’m trying to cause an out-of-heap-memory error, with the following code:

class Person
   def initialize(name : String)
      @name = name
   end

   def name
      @name
   end

end

count = 0

while count <= 100000000000
  p = Person.new("a")
  p.name
  count += 1
end

And then running it with -Dgc_none.

But I get no errors :confused:

Then Crystal is not a good fit for your use case. I suggest using Rust. Maybe also take a look at Zig. And maybe Go could also be useful here, not sure! But Crystal is not built for low-latency systems programming, and there’s no way it will ever be.

What error were you expecting?

I think -Ggc_none is recommended a lot on the internet, but I wish that would stop. That flag only exists to debug memory issues related to the GC. It’s not meant to be used for anything else.

2 Likes

I respectfully hope that you are wrong, and other members of the core team disagree. Isn’t Crystal supposed to be a very fast language? So why not make it suitable for real-time programming?

I also think you are overestimating the effort. All you have to do is give an option to turn off the GC. And memory management can be done manually.

This is just an opportunity that Crystal should not miss.

Try this to get what you’re asking for:

class Person
  def initialize(name : String)
    @name = name
  end

  def name
    @name
  end
end

count = 0u64

while count <= 100000000000u64
  p = Person.new("a")
  p.name
  count += 1
end

puts p

And then run it with crystal build -Dgc_none program.cr; ./program (and you can add an optional --release after crystal build). That should get you the OoM you’re looking for (it’ll just show “Terminated” as output, though, once the OS kills the process).

Your code will overflow your count variable (which is an Int32, the default integral primitive in Crystal) before you get an OoM crash (probably), so I changed the count to a UInt64. I also added code that actually uses p, which helps get around optimizations during compilation which remove unused code (which may only happen if you compile with --release).


I agree with @asterite, though, that Crystal just isn’t a great option for your use case. Maybe someone could rewrite the language to use manual memory management or a borrow checker or something, but that would just be a new language.

… until someone builds an alternative standard library with manual memory management :man_shrugging:

Never say never :laughing: There have definitely been some endevours in that direction (example: https://github.com/ffwff/lilith/tree/master/src/core).

But honestly, I’d like to stress that the bare language should be very well suite for such tasks. Just the stdlib isn’t. It focuses on usability which is a loss for efficient memory management. And that will certainly never change.

But when you have an alternative stdlib, you can swap it. Maybe that’s not completely original Crystal anymore, but it’s at least in part.
Besides the standard library, shards are another issue, though.

Crystal is foremost supposed to be easy to use. Unfortunately, that contradicts with manual memory management, because humans are not good at applying that. Crystal happily trades that extra efficiency for better usability by using automated garbage collection. Which makes it still pretty darn fast, by the way. Despite using a very generic GC library.

I can’t see how that would work. We have an option to turn off the GC. But all code expects it to be there. So how do you solve that?

3 Likes

Oh, I see now. You are talking about the stdlib. What does the stdlib include in Crystal? Java has the exact same problem on its java.util package, which has its collections (lists, hashmaps, sets, iterators, etc). The solution is easy, you simply do not use any of those. There are real-time data structures libraries out there that you can use, which produce zero garbage and do not rely on the GC.

This is defined by the prelude file, which by default is https://github.com/crystal-lang/crystal/blob/master/src/prelude.cr. However it’s possible to provide your own, which would be a requirement for a fully manual memory managed stdlib.

2 Likes

I’ve heard this quite a bit, but I’m curious about numbers. What sort of latency requirements do you have? How certain are you that Crystal’s GC pushes it above your threshold?

2 Likes