Skip to content

Commit

Permalink
update memory model
Browse files Browse the repository at this point in the history
  • Loading branch information
mertcandav committed Sep 26, 2024
1 parent ebfbadf commit 287f23d
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 140 deletions.
2 changes: 1 addition & 1 deletion .vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export default defineConfig({
text: 'Memory',
link: '/memory/',
items: [
{ text: 'Initialization', link: '/memory/initialization' },
{ text: 'Memory Model', link: '/memory/memory-model' },
{ text: 'Slicing', link: '/memory/slicing' },
{ text: 'Immutability', link: '/memory/immutability' },
{ text: 'Mutability', link: '/memory/mutability' },
Expand Down
15 changes: 15 additions & 0 deletions src/api/runtime-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,31 @@ jule::Uint __jule_RCLoad(jule::Uint *p);
```
Declaration of: `_RCLoad`

```cpp
jule::Uint __jule_RCLoadAtomic(jule::Uint *p);
```
Declaration of: `_RCLoadAtomic`
```cpp
void __jule_RCAdd(jule::Uint *p);
```
Declaration of: `_RCAdd`

```cpp
void __jule_RCAddAtomic(jule::Uint *p);
```
Declaration of: `_RCAddAtomic`
```cpp
jule::Bool __jule_RCDrop(jule::Uint *p);
```
Declaration of: `_RCDrop`

```cpp
jule::Bool __jule_RCDropAtomic(jule::Uint *p);
```
Declaration of: `_RCDropAtomic`
```cpp
void __jule_RCFree(jule::Uint *p);
```
Expand Down
16 changes: 1 addition & 15 deletions src/common-concepts/functions/anonymous-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,4 @@ fn main() {
outln(i) // 2
}
```
In the above example, we know that the closure will live shorter than the scope and we want to capture by reference. A child scope is created, but this is not necessary. This is an improvement to prevent the reference variables we created for the variables we want to capture by reference from surviving in the rest of the scope. The `ri` variable is used to reference the `i` variable and is mutated with Unsafe Jule in the closure. In this way, the `i` variable is also affected.

## Technical Details

Anonymous functions and closures are usually cheap. An anonymous function or closure is not much different from ordinary functions. They are implemented the same way in the background, only the name is automatically chosen by the compiler.

Anonymous functions and closures are stored and moved with pointers, just like ordinary functions. An anonymous function or closure has no additional cost to call. For anonymous functions, there is not additional execution cost, unlike closures.

Closures impose some additional costs. One of these is captured variables. A heap allocation is created for captured variables and is guaranteed to be traced and deallocated by the GC. There is no risk of memory leak. A closure is automatically deallocated when it is no longer reachable. There are no other memory footprints.

Captured variables are automatically detected by the compiler and captured by copy. More variables than necessary are not captured; the compiler captures only the variables used in the closure.

Due to the use of closures, all anonymously used functions have an additional hidden parameter. This hidden parameter is a pointer with common type traced by the GC and is null for functions other than other closures. If a ordinary function is used as an anonymous function, the compiler adds this hidden parameter for it. This parameter is only required for closures and is handled only by closures, other functions will ignore it.

A few additional instructions are added to the closure to handle this parameter. These instructions are usually cheap and do not impose significant runtime costs. The instructions usually include converting the hidden parameter to the closure's environment data.
In the above example, we know that the closure will live shorter than the scope and we want to capture by reference. A child scope is created, but this is not necessary. This is an improvement to prevent the reference variables we created for the variables we want to capture by reference from surviving in the rest of the scope. The `ri` variable is used to reference the `i` variable and is mutated with Unsafe Jule in the closure. In this way, the `i` variable is also affected.
5 changes: 0 additions & 5 deletions src/compiler/compiler-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ Disable reference counting. All reference counted types does not perform referen
`--disable-safety`\
Disable safety. All memory safety and similar measures will be disabled. This will increase safety risks, but at the same time it might improve runtime performance. It may be helpful for debugging, see [Debugging](/debugging/) section for more information.

---

`--atomic-rc`\
Enables thread-safe atomic reference counting.

### Optimization Options

Learn more about [compiler optimizations](/compiler/compiler-optimizations).
Expand Down
1 change: 0 additions & 1 deletion src/compiler/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Here is the list of variables and their existence:
- `x64`: 64-bit cpu architecture
- `production`: production compilation enabled
- `test`: compiling for testing
- `atomicrc`: atomic reference counting
- `clang`: backend compiler is Clang
- `gcc`: backend compiler is GCC
- `cpp14`: using C++14 standard
Expand Down
17 changes: 1 addition & 16 deletions src/dynamic-types/any.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,4 @@ let x = (int)(myAny)
For example, you have slice value holds by any-typed variable.
And your variable is immutable.
So, if you cast your value to slice for assign to mutable variable, you will get error.
Because of slice is mutable type, so it's breaking immutability.

## Technical Details

The Any type always stores its data on the heap and is guaranteed to be released by the GC. Since the type is ambiguous, it has some additional costs on top of memory.

At runtime, an `any` stores and uses 2 different data;

- **Allocation**\
Allocation is a pointer to the data itself that the `any` stores. Managed by GC. The current implementation handles this well. If a pointer that is already traced by the GC is passed to the `any`, for example a smart pointer, the `any` uses it by directly referencing that smart pointer rather than making a new allocation. This helps reduce memory allocations and increases efficiency.\
\
If given smart pointer is `nil`, then the `any` will be `nil`. Will not point to the smart pointer. Any type always tries to use smart pointers as base allocation and shares same memory.
- **Type Pointer**\
An `any` maintains a general pointer and this pointer is not traced by the GC because it is guaranteed to always will point to static memory that will be available for the lifetime of the program. This pointer points directly to the type handler structure automatically created by the compiler.\
\
The handler structure includes the deallocator function required for the type. The deallocator function is the first field of the structure. Also contains 2 function pointer for string conversion and comparison functions for stored type.
Because of slice is mutable type, so it's breaking immutability.
19 changes: 1 addition & 18 deletions src/dynamic-types/traits/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,4 @@ fn main() {
let c: Bar = a
c.bar()
}
```

## Technical Details

Using traits is relatively cheap. To meet dynamic typed programming requirements, your compiler tries to obtain the necessary instructions at minimum cost.

At runtime, a trait stores and uses 3 different data;

- **Allocation**\
Allocation is a pointer to the data itself that the trait stores. Managed by GC. The current implementation handles this well. If a pointer that is already traced by the GC is passed to the trait, for example a smart pointer, the trait uses it by directly referencing that smart pointer rather than making a new allocation. This helps reduce memory allocations and increases efficiency.\
\
If given smart pointer is `nil`, then the trait will be `nil`. Will not point to the smart pointer. Traits always tries to use smart pointers as base allocation and shares same memory.
- **Pointer State**\
Traits may take both a smart pointer or a normal instance for supported types. Accordingly, the deallocation method and type comparison also vary. If separate code was generated for both forms with and without smart pointers, this could significantly increase the size and compilation time of the executable. Since it does not contribute much to the runtime cost, it stores whether the stored data is a smart pointer or not with a simple boolean flag. In this way, it is sufficient to generate a single handler for each form of trait type.
- **Type Pointer**\
A trait container maintains a general pointer and this pointer is not traced by the GC because it is guaranteed to always will point to static memory that will be available for the lifetime of the program. This pointer points directly to the type handler structure automatically created by the compiler. The handler structure includes the deallocator function required for the type. The deallocator function is the first field of the structure. In this way, with a simple reinterpretation, the trait container can call the deallocator function when necessary. Each time trait is used, the type pointer is reinterpreted for the correct type, providing direct access to the required function.

The compiler defines additional wrapper functions so that the trait calls the right function of the right type. These wrapper functions are stored in conjunction with the handle structure pointed to by the type pointer. When a trait method called, trait calls the required wrapper function to performs the necessary type conversion and redirects to the original function.
```
8 changes: 1 addition & 7 deletions src/dynamic-types/traits/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,4 @@ fn main() {
a.Baz()
}
```
Example at above, the `Baz` trait inherits the `Foo` and `Bar` traits. Additionally defines the `Baz` method. The structure will implement the `Foo` and `Bar` traits too when implements the `Baz` trait.

## Technical Details

Inheritance cost is relatively directly proportional to how many implemented structures there are. However, this is an algorithm that performs the minimum effort required, so most of the time there is no need to worry about overhead.

The runtime cost for Inheritance only occurs when converting between inherited traits. Apart from this, there is no additional cost in runtime. Your compiler defines a mapper function for the correct conversions and uses this mapper function during casting to justify the trait's type pointer conversion for the required trait data container.
Example at above, the `Baz` trait inherits the `Foo` and `Bar` traits. Additionally defines the `Baz` method. The structure will implement the `Foo` and `Bar` traits too when implements the `Baz` trait.
16 changes: 1 addition & 15 deletions src/integrated-jule/interoperability/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,4 @@ unsafe {
cpp.printf((*integ::Char)(&s[0]))
}
```
The version of the above example using string. While this is not recommended, it is something that can be done for important reasons such as efficiency and performance concerns, but Jule does not guarantee this and the responsibility lies with the developer.

### Why Stings are Length-Based

Jule aims for high interoperability capabilities, but the primary goal is to have good language, not to serve interoperability. For this reason, some design choices are made without considering interoperability for important reasons such as performance and efficiency.

Jule strings are designed to be immutable to prevent a new allocation on each copy. They become heap allocated only as a result of operations such as concatenation at run time. Jule's built-in memory management mechanisms are used for heap allocated strings.

Accordingly, strings, like slices, can be moved very efficiently, sharing common memory areas and avoiding new allocations when slicing. However, this can only achieve the desired efficiency with a length-based design.

**Here are a few reasons why:**
- Jule algorithms work length-based, so NULL termination is unnecessary. NULL termination is just something that could be added to make C-string conversions safe regarding interoperability. This will reduce the performance and efficiency of Pure Jule code, even for developers who will not use interoperability at all.
- Slicing can be done with NULL terminated strings without causing a new allocation if it preserves the last character of the string. However, if it does not, it is necessary to make a new allocation because null termination is lost and since the string memory is shared, it is not safe to replace the last character with NULL termination, so most cases will result in a new allocation, except for some optimizable cases.
- With unsafe algorithms, strings can be converted into byte-slices and new allocations in operations such as slicing can be avoided. However, the Unsafe Jule in this code is very useful in encouraging and increasing its use. Many algorithms may require slicing of strings, which may require unsafe conversions in many places in the codebase. It is not a simple experience.
- In some fields it may be necessary to return byte-slice allocations as strings. In this case, if you are sure it is safe, you may want to unsafely convert it to string to avoid new allocation. But before doing this, you need to make sure that there is a NULL termination every time. Besides this increasing the developer's considerations, perhaps that single NULL termination you add to the slice will in some cases exceed the capacity and result in new allocation, in which case it's not much different than converting to string.
The version of the above example using string. While this is not recommended, it is something that can be done for important reasons such as efficiency and performance concerns, but Jule does not guarantee this and the responsibility lies with the developer.
25 changes: 0 additions & 25 deletions src/memory/initialization.md

This file was deleted.

20 changes: 0 additions & 20 deletions src/memory/management/smart-pointers.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,26 +115,6 @@ fn main() {
} // Frees allocation because ref count is 0, destroyed all references
```

## Critical Points
Jule has language-level concurrency and reference counting should be atomic for safe concurrency. Reference counting may not occur correctly if there is no atomicity in concurrency. That is, when a reference is referenced by a different reference, it must do so in an atomic way. But the fact that this happens all the time raises a problem: the critical impact on performance.

Atomic operations are essential for references to be thread-safe, but in cases where this is not necessary? Atomicity means overhead, which means loss of performance. It is inherently unnecessary to have atomicity when atomicity is not required. Jule references works atomic because thread-safe must be guaranteed.

This means that references will use atomicity for reference counting on each copy. This atomicity creates an atomicity overhead in memory with each copy operation. Obviously this shouldn't be a major cause of performance degradation in your runtime in most cases. However, references also contain a memory footprint. This memory footprint is the memory space allocated separately for the counter used in reference counting.

All of these are minor overheads, but for performance-critical software, the developer may want to eliminate them. Smart pointers that do not do reference counting can be obtained with Unsafe Jule, so this burden can be alleviated up to a point, but there is no built-in feature to remove it completely.

---

Some data types of Jule also use smart pointers in the background. This is because they reference each other the space they allocate. This is why some types use background smart pointers to minimize the amount of allocations. Therefore, they have additional overhead such as the additional atomicity of smart pointers and the memory space allocated for reference counting.

List of all types which is performs internal reference counting:
- Smart Pointers
- Slice
- Trait
- Any
- Str (for only heap allocated strings)

## Using Smart Pointers with Reference-Counted Types
Data types that already perform reference counting can be used with smart pointers if supported. This does not pose any problem. Smart pointers perform a reference counting in themselves, if the data they carry has a reference counting, it does not interfere with them.

Expand Down
Loading

0 comments on commit 287f23d

Please sign in to comment.