Skip to content
Paolo Angeli edited this page Oct 27, 2019 · 9 revisions

Memory Management

Jai does not and will never feature garbage collection or any kind of automatic memory management.

STRUCT POINTER OWNERSHIP

Marking a pointer member of a struct with ! indicates that the object pointed to is owned by the struct and should be deleted when the struct is deallocated. Example:

node :: struct {
    owned_a : !* node = null;
    owned_b : !* node = null;
}

example: node = new node;
example.owned_a = new node;
example.owned_b = new node;

delete example; // owned_a and owned_b are also deleted.

Here, owned_a and owned_b are marked as being owned by node, and will be automatically deleted when the node is deleted. In C++ this is accomplished through a unique_ptr<T>, but Jai considers this the wrong way to do it, because the template approach now masks the true type of the object. A unique_ptr<node> is no longer a node—it’s a unique_ptr masquerading as a node. It’s preferable to retain the type of node*, and retain the properties of node*-ness that go along with it, because we don’t really actually care about unique_ptr for its own sake.

CUSTOM ALLOCATORS

For every application that is aimed at optimal performance, memory allocation is a very important feature. Dealing way too often directly with the operative system to get memory and manage the heap may result in large loss of performance. Many modern languages are garbage collected so many programmers don't even know how important memory management is.

In Jai, memory management is taken really seriously, and programmers have full and real control over this.

In Jai like in other languages, it is possible to define custom allocators that are responsible to manage the lifecycle of data in memory.

In Jai's "basic" library, are available several useful allocators with their data structures and related subroutines.

One of the most common and useful is Pool allocator. A pool allocator requests a predefined amount of memory to the operative system, and then, it is responsible to give portions of this memory to the data structures that might need it. This technique allows almost inexpensive memory allocation overhead for the data in the pool.

Pools are used to manage data structures of different sizes but approximately with the same lifetime.

Internally the data structure that describes the Pool contains several fields that keep track of the operations and how to lay down the data in memory and align the bits to get the best performance and cache friendliness.

Allocations are done sequentially resulting in very fast operations.

Using the Pool, single or atomic memory release or free operations are not supported. The deallocation is made all at once when the pool is no longer required.

This leads to a different memory access pattern against the classic heap memory management model.

This is only an example. The programmer has total freedom to create new allocators and associate them with different data types. So for instance, it is possible to do something like this

#import "Pool"

// a new Pool
pool: Pool;
// Pass the pointer to the pool to the current allocator 
set_allocators(*pool);
// this call will release the entire pool when this scope will close
defer release(*pool);

// a variable length array of integers
many_numbers: [..]int;
// set the funtion that will be called when a new item will be added to the array
many_numbers.allocator = pool_allocator;
// set the memory where the items will reside
many_number.allocator_data = pool;
// Now every insertion to the variable lenght array will use the Pool allocator.
N::2000;
for 1..N array_add(*many_numbers, cast(int)it );

… this section is not written yet! Sorry. (Jai provides mechanisms for managing the allocations of an imported library without requiring work from the library writers.)

INITIALIZATION

By default, every allocated data ( both on the heap or on the stack, even fields in data structures) is automatically initialized to zero. By doing this, Jai gives a defined consistent behavior that the programmer can leverage to reduce cognitive load. Since this has a processing overload, that sometimes can be superfluous, Jai has the capability to explicitly define that the allocated memory will be uninitialized. So the programmer has a further bit of space for potential optimization.

Let see some examples:

// Define a data structure
Vector3 :: struct {
    x: float;
    y: float;
    z: float;
}
// Allocate a variable on the stack
v : Vector3;
// print values
print("v.x=%, v.y=%, v.z=%\n", v.x, v.y, v.z); 
  
$ v.x=0, v.y=0, v.z=0

For data structures, it is possible to mimic default constructors used in OOP languages. Jai gives to the programmer the capability to define default special initialization values:

// Define a silly data structure with special initial values 
Up3DVector :: struct {
    x: float = 0;
    y: float = 1;
    z: float = 0;
}
// Allocate a variable on the stack
up : Up3DVector;
print("%-%-%\n", v.x, v.y, v.z); 

$ 0-1-0

// Declare and allocate a matrix of 10000 Up3DVector on the stack
floor_default_normals : [100,100] Up3DVector;
// Print the data of a specific item in the matrix
using floor_default_normals [50,50]{
  print("% % %\n", x, y, z);
}
$ 0-1-0

For special cases when the programmer knows that initialization is unnecessary or redundant, it is possible to specify explicitly to the compiler to not initialize variables on allocation. This can be done at the data structure definition:

RawVector3 :: struct {
    x: float = ---;
    y: float = ---;
    z: float = ---;
}
// Declare and allocate a RawVector3  on the stack
v : RawVector3;
// WARNING!! Undefined behavior. Could print anything.
print("% % %\n", v.x, v.y, v.z); 

Or at variable declaration:

#import "Basic"

// Define a data structure with special initialization
Random3 :: struct {
    x: float = random(-1.0, 1.0);
    y: float = random(-1.0, 1.0);
    z: float = random(-1.0, 1.0);
}
// Declare and allocate a Random3 on the stack but don't initialize it
v : Random3 = ---;
// WARNING!! Undefined behavior. Could print anything.
print("% % %\n", v.x, v.y, v.z);                 

// Declare and allocate an array of 100 Random3 on the stack 
// but don't initialize any of those
va : [100] Random3 = ---;
// WARNING!! Undefined behavior. Could print anything.
print("% % %\n", va[50].x, va[50].y, va[50].z); 

Types, constants and variables

  • Variables and assignments
  • Language data types
  • Simple user-defined data types
  • Expressions and operators
  • Type-casting
  • Pointers

Flow control

Procedures and functions

  • Declarations
  • Arguments / Parameters
  • Return values
  • Overloading / Polymorhism
  • Advanced features
  • Lambdas

Aggregated data types

  • Arrays
  • Strings
  • Composition of Structs

Advanced features

Clone this wiki locally