When writing C bindings in Crystal, one question often arises.
Should an object yielded by each be newly created on every iteration, or should the same object be reused?
My understanding of this issue is still incomplete, but I would like to summarize my current thoughts.
First, why use Crystal rather than Ruby to write C bindings?
The main reason is performance.
Memory allocation is an important consideration when writing high-performance Crystal code. Crystal uses garbage collection, and memory allocation and garbage collection have associated costs. It is also difficult to control exactly when memory is reclaimed.
These costs are usually not a major concern, but they can become important when writing C bindings.
Consider iterating over a collection:
bam.each do |record|
p record
end
To execute this code efficiently, should a new record object be allocated on every iteration?
If a new object is created each time, and the object refers to memory managed by C, should the underlying data also be copied?
One important consideration is compatibility with Enumerable.
include Enumerable
To use Enumerable safely, objects yielded by each must remain valid when they are stored for later use.
For example, select and to_a collect elements into an array. If the same object is reused on every iteration, the array will contain multiple references to that object.
Therefore, when performance is not critical, creating independent objects is a reasonable choice.
However, when performance is important, the situation is different. Benchmarks show that memory allocation, copying, and garbage collection can significantly reduce performance.
In Ruby, safety would generally take priority. In Crystal, however, performance closer to C is often an important goal. It is therefore desirable to avoid unnecessary memory allocation and garbage collection.
A similar design can be found in Crystal's standard library: Enumerable#each_slice.
a.each_slice(3, reuse: true) { |s| p s }
By default, each_slice creates a new array on each iteration. When reuse: true is specified, the same array is reused.
This allows the programmer to explicitly choose object reuse when performance is important.
However, using an argument to switch between different behaviors within the same method may complicate implementation, type inference, and optimization.
Another approach is to provide separate methods. Possible names include:
eacheach_reuseeach_vieweach_copy
There does not yet appear to be an established naming convention for this distinction in the Crystal community.
Another possible design is to make each reuse objects by default and implement the necessary Enumerable-like methods separately. This approach gives priority to performance.
It is important to distinguish between reusing a Crystal object and borrowing memory from C. These are different concepts. Whether reuse or view is the appropriate name depends on how memory ownership is managed.
Other related questions remain. For example, should Record implement finalize? Who should own the memory allocated by C, and when should that memory be released?
(This post was translated from the original Japanese post using ChatGPT)
Top comments (0)