Inko 0.6.0 has been released
Inko 0.6.0 has been released, featuring improvements to the garbage collector, additions to the standard library, more efficient sending of messages between processes, and various other changes.
Table of contents
- New standard library additions
- Renaming of the AST Expressions type
- Removal of asynchronous finalisation
- Garbage collector bug fixes and performance improvements
- Reworked sending of process messages
- New environment variables for tuning performance
- Inko now requires 64-bits
If you would like to support the development of Inko, please donate to Inko on Open Collective or via GitHub Sponsors. If you would like to donate via other means, please contact us through Email or GitHub.
If you would like to engage with others interested in the development of Inko, please join the Matrix chat channel. You can also follow the development on Reddit, or follow the author of Inko on Twitter. For more information check out the Community page.
New standard library additions
Inko 0.6.0 adds various new methods and types to the standard library.
Object.not_nil?
The method not_nil?
is available to all objects defined, and allows you to
check if a value is Nil
or not. This method is useful when you have an
optional type (e.g. ?String
) and you want to explicitly check if the value is
Nil
or not:
Nil.not_nil? # => False
String.not_nil? # => True
let number: ?Integer = 10
number.not_nil? # => True
Using this method you no longer have to write thing.nil?.not
, instead you can
just use thing.not_nil?
.
Iterator.any?
The method any?
has been added to std::iterator::Iterator
. This method takes
a closure and will return True
if the closure returns True
for a value in
the Iterator
, stopping iteration when it finds such a value:
let numbers = Array.new(10, 20, 30)
numbers.any? do (number) { number > 10 } # => True
numbers.any? do (number) { number > 30 } # => False
Iterator.select?
The method any?
has been added to std::iterator::Iterator
. This method takes
a closure and will produce a new Iterator
that includes every value for which
the closure returned True
:
let numbers = Array.new(10, 20, 30)
numbers.iter.select do (number) { number > 10 }.to_array # => Array.new(20, 30)
Integer.times
The method times
has been added to std::integer::Integer
. This method
returns an Iterator
that produces values ranging from zero to (but not
including) the integer value:
4.times.to_array # => Array.new(0, 1, 2, 3)
This method is useful when you want to call the same closure several times:
4.times.each do (number) {
# ...
}
If you just want to call the closure and don't care about the integer argument
passed to it, you can define the argument name as _
:
4.times.each do (_) {
# ...
}
For now this won't do anything special, but in the future this will ensure the compiler adds no warning if the argument is not used anywhere.
Pair and Triple types
The module std::pair
has been added, defining the types Pair
and Triple
. A
Pair
is a tuple of two values, and a Triple
is a tuple of three values. Inko
does not support N-ary tuples (or tuples with more than three values), as custom
types created using the object
keyword are a better fit for such cases.
Iterator.partition
The method partition
has been added to std::iterator::Iterator
. This method
is used to partition an Iterator
into a Pair
of two arrays: an array
containing values for which a provided closure returned True
, and an array
containing values for which the closure returned False
:
let numbers = Array.new(10, 20, 30, 40)
let partition = numbers.iter.partition do (value) { value < 30 }
partition.first # => Array.new(10, 20)
partition.second # => Array.new(30, 40)
String.byte
The method byte
has been added to std::string::String
. This method can be
used to get a single byte given a byte index. This is useful when you want to
extract bytes from a String
, but don't want to allocate a ByteArray
:
'inko'.byte(0) # => 105
Path.join
The method join
has been added to std::fs::path::Path
. This method can be
used to join a Path
with a type that implements std::fs::path::ToPath
, such
as another Path
or a String
:
import std::fs::path::Path
Path.new('foo').join('bar') # => Path.new('foo/bar')
This method supports both Unix and Windows file path separators.
Path.absolute?
The method absolute?
has been added to std::fs::path::Path
. This method
returns True
for an absolute path, False
otherwise:
import std::fs::path::Path
Path.new('foo').absolute? # => False
Path.new('/foo').absolute? # => True
Path.relative?
The method relative?
has been added to std::fs::path::Path
. This method
returns True
for a relative path, False
otherwise:
import std::fs::path::Path
Path.new('foo').relative? # => True
Path.new('/foo').relative? # => False
Renaming of the AST Expressions type
The type std::ast::expressions::Expressions
has been renamed to Body
, and
the module has been renamed to std::ast::body
.
Removal of asynchronous finalisation
Various Inko object use data structures that need to be finalised/deallocated when the Inko object is garbage collected. Before Inko 0.6.0, a separate pool of threads was used to finalise these objects some time after they became garbage.
Starting with Inko 0.6.0, this mechanism has been removed. Instead, objects are now finalised when their memory is reused after they have been garbage collected. This simplifies the virtual machine, and for most programs should not impact memory usage.
When a process terminates we still finalise its objects right away, but this may change in the future.
Garbage collector bug fixes and performance improvements
For Inko 0.6.0 we rewrote various parts of the garbage collector. In the progress report for October 2019 we discussed how we ran into some garbage collector bugs in October, and how they were severe enough that we could not ignore them. We are pleased to announce that all bugs we have found have been fixed, and that we have also improved the performance of our parallel generational garbage collector.
The garbage collector improvements are attributed to two big changes:
- We use a new strategy for remembering cross generational pointers.
- We now trace objects in parallel, instead of only tracing stack frames in parallel.
The new strategy for cross generational pointers is discussed in detail in the October progress report. In short, we use a chunked list for remembering mature pointers, and a per-object bit to prevent duplicates. When promoting objects we use a specialised tracing procedure. When this procedure encounters a young pointer, we remembered the promoted (and now mature) object in the remembered set; instead of remembering all promoted objects.
The parallel tracing changes can greatly improve performance for large heaps, though even small heaps will benefit. Before Inko 0.6.0 we only processed stack frames in parallel, tracing all objects in those frames sequentially. While this was easy to implement, it only provided limited performance improvements over performing all work sequentially. In our new setup we trace objects in parallel, using a pool of tracer threads that use work stealing. Instead of using a fixed-size thread pool, we spawn these threads on the fly. In the future we may decide to also reuse OS threads in some way, removing the overhead that comes with spawning OS threads.
For small heaps we expect garbage collection timings between 500 microseconds and 2 milliseconds. For larger heaps we expect that garbage collection will take between 1 and 10 milliseconds. These timings will vary based on the number of objects that need to be promoted and/or evacuated (to combat fragmentation), as doing so is expensive and involves some synchronisation.
As an example: using the default garbage collection settings, Inko's own test suite never triggers garbage collection, as tests don't allocate enough memory before they finish. If we reduce the garbage collection threshold from the default 1024 memory blocks to 32 blocks, most collections take less than one millisecond.
In the future we can implement other performance improvements, such as dynamically adjusting the number of garbage collection threads. For now we will postpone such improvements so we can instead focus on working towards a self-hosting Inko compiler.
For more information about all the garbage collector changes, consider taking a look at commit 347ade.
Reworked sending of process messages
Before Inko 0.6.0, sending a message to a process involved two steps:
- The message was copied into a separate mailbox heap owned by the receiving process.
- The receiving process would copy the message from the mailbox heap into its local heap.
These steps ensured a process could never directly refer to memory in the mailbox, which simplified the garbage collector implementation.
Starting with Inko 0.6.0 processes that send messages allocate directly into the local heap of a process, using a lock to ensure this does not happen when the receiving process is garbage collected. The receiving process never uses a lock at run time. Using this approach we reduce the amount of memory every process needs when receiving messages, and we can remove a lot of code related to garbage collecting process mailboxes.
In the current implementation, a process that is garbage collected will block other processes from sending messages to it, until garbage collection finishes. In the future we aim to improve this by rescheduling the sending processes if they can not acquire the lock.
New environment variables for tuning performance
As part of the garbage collection changes we have changed several of the
environment variables used to tweak various virtual machine settings. The
INKO_CONCURRENCY
variable has been replaced with the following variables:
INKO_PRIMARY_THREADS
: controls the number of threads used for running regular processes.INKO_BLOCKING_THREADS
: controls the number of threads used for running processes that perform blocking operations.INKO_GC_THREADS
: controls the number of threads used in the fixed-size garbage collection coordination thread pool.INKO_TRACER_THREADS
: controls the number of threads spawned for tracing objects. Each process collected will have its own pool of tracers, spawned when needed and terminated when all work is done.
Both INKO_GC_THREADS
and INKO_TRACER_THREADS
default to half the number of
CPU cores, to reduce the amount of threads fighting over CPU time.
Inko now requires 64-bits
The instruction of the new remembered set requires us to tag a third bit in a particular pointer. On 32-bits platforms only the lower two bits are unused, while on 64-bits platforms the lower three bits are unused. Since we need to make use of a third bit, Inko now requires 64-bits platforms.