mirror of
https://github.com/fluencelabs/assemblyscript
synced 2025-04-25 07:02:13 +00:00
Add what we have for GC so far
Not functional yet, but might be good to have for discussing it.
This commit is contained in:
parent
a15457d8b9
commit
9579086699
@ -5,17 +5,15 @@
|
||||
[](https://travis-ci.org/AssemblyScript/assemblyscript)
|
||||
[](https://build.snapcraft.io/user/AssemblyScript/assemblyscript)
|
||||
|
||||
**AssemblyScript** compiles strictly typed [TypeScript](http://www.typescriptlang.org) to [WebAssembly](http://webassembly.org) using [Binaryen](https://github.com/WebAssembly/binaryen). It generates lean and mean WebAssembly modules while being just an `npm install` away.
|
||||
**AssemblyScript** compiles strictly typed [TypeScript](http://www.typescriptlang.org) (basically JavaScript with types) to [WebAssembly](http://webassembly.org) using [Binaryen](https://github.com/WebAssembly/binaryen). It generates lean and mean WebAssembly modules while being just an `npm install` away.
|
||||
|
||||
See [the AssemblyScript wiki](https://github.com/AssemblyScript/assemblyscript/wiki) for further instructions and documentation. You can also try it out in [WebAssembly Studio](https://webassembly.studio)!
|
||||
See [the AssemblyScript wiki](https://github.com/AssemblyScript/assemblyscript/wiki) for instructions and documentation. You can also try it out in [WebAssembly Studio](https://webassembly.studio)!
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
A few early examples to get an idea:
|
||||
|
||||
* **[Conway's Game of Life](./examples/game-of-life)**<br />
|
||||
Continuously updates the cellular automaton and visualizes its state on a canvas.
|
||||
Continuously updates the cellular automaton and visualizes its state on a canvas. There's also [this WebAssembly Studio fiddle](https://webassembly.studio/?f=gvuw4enb3qk) of it.
|
||||
|
||||
* **[i64 polyfill](./examples/i64-polyfill)**<br />
|
||||
Exposes WebAssembly's i64 operations to JavaScript using 32-bit integers (low and high bits).
|
||||
|
2
dist/asc.js
vendored
2
dist/asc.js
vendored
File diff suppressed because one or more lines are too long
2
dist/asc.js.map
vendored
2
dist/asc.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/assemblyscript.js.map
vendored
2
dist/assemblyscript.js.map
vendored
File diff suppressed because one or more lines are too long
@ -40,7 +40,6 @@ import {
|
||||
|
||||
import {
|
||||
ElementKind,
|
||||
Global,
|
||||
FunctionPrototype,
|
||||
Class,
|
||||
Field,
|
||||
|
2
std/assembly.d.ts
vendored
2
std/assembly.d.ts
vendored
@ -26,7 +26,7 @@ declare type u64 = number;
|
||||
/** A 32-bit unsigned integer when targeting 32-bit WebAssembly or a 64-bit unsigned integer when targeting 64-bit WebAssembly. */
|
||||
declare type usize = number;
|
||||
/** A 1-bit unsigned integer. */
|
||||
declare type bool = any; // sic
|
||||
declare type bool = boolean | number;
|
||||
/** A 32-bit float. */
|
||||
declare type f32 = number;
|
||||
/** A 64-bit float. */
|
||||
|
253
std/assembly/collector/itcm.ts
Normal file
253
std/assembly/collector/itcm.ts
Normal file
@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Incremental Tri-Color-Marking Garbage Collector.
|
||||
*
|
||||
* @module std/assembly/collector/itcm
|
||||
*//***/
|
||||
|
||||
// Based on the concepts of Bach Le's μgc, see: https://github.com/bullno1/ugc
|
||||
|
||||
import {
|
||||
AL_MASK,
|
||||
MAX_SIZE_32
|
||||
} from "../internal/allocator";
|
||||
|
||||
// ╒═══════════════ Managed object layout (32-bit) ════════════════╕
|
||||
// 3 2 1
|
||||
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 bits
|
||||
// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┤ ┐
|
||||
// │ next │ F │ ◄─┐ = nextWithFlags
|
||||
// ├─────────────────────────────────────────────────────────┴─────┤ │ usize
|
||||
// │ prev │ ◄─┘
|
||||
// ╞═══════════════════════════════════════════════════════════════╡ SIZE ┘
|
||||
// │ ... data ... │
|
||||
// └───────────────────────────────────────────────────────────────┘
|
||||
// F: flags
|
||||
|
||||
/** Managed object flags. */
|
||||
namespace Flags {
|
||||
/** Object is unreachable (so far). */
|
||||
export var WHITE = 0;
|
||||
/** Object is reachable. */
|
||||
export var BLACK = 1;
|
||||
/** Object is reachable but its children have not yet been scanned. */
|
||||
export const GRAY = 2;
|
||||
/** Mask to obtain just the flag bits. */
|
||||
export const MASK = AL_MASK;
|
||||
}
|
||||
|
||||
/** Represents a managed object in memory, consisting of a header followed by the object's data. */
|
||||
@unmanaged
|
||||
class ManagedObject {
|
||||
|
||||
/** Pointer to the next object with additional flags stored in the alignment bits. */
|
||||
nextWithFlags: usize;
|
||||
|
||||
/** Pointer to the previous object. */
|
||||
prev: ManagedObject;
|
||||
|
||||
/** Visitor function called with the data pointer (excl. header). */
|
||||
visitFn: (obj: usize) => void;
|
||||
|
||||
/** Size of a managed object after alignment. */
|
||||
static readonly SIZE: usize = (offsetof<ManagedObject>() + AL_MASK) & ~AL_MASK;
|
||||
|
||||
/** Gets the pointer to the next object in the list. */
|
||||
get next(): ManagedObject {
|
||||
return changetype<ManagedObject>(this.nextWithFlags & ~Flags.MASK);
|
||||
}
|
||||
|
||||
/** Sets the pointer to the next object in the list. */
|
||||
set next(obj: ManagedObject) {
|
||||
this.nextWithFlags = changetype<usize>(obj) | (this.nextWithFlags & Flags.MASK);
|
||||
}
|
||||
|
||||
/** Inserts an object to this list. */
|
||||
insert(obj: ManagedObject): void {
|
||||
var prev = this.prev;
|
||||
obj.next = this;
|
||||
obj.prev = prev;
|
||||
prev.next = obj;
|
||||
this.prev = obj;
|
||||
}
|
||||
|
||||
/** Removes this object from its list. */
|
||||
remove(): void {
|
||||
var next = this.next;
|
||||
var prev = this.prev;
|
||||
next.prev = prev;
|
||||
prev.next = next;
|
||||
}
|
||||
|
||||
/** Tests if this object is white, that is unreachable (so far). */
|
||||
get isWhite(): bool {
|
||||
return (this.nextWithFlags & Flags.MASK) == Flags.WHITE;
|
||||
}
|
||||
|
||||
/** Marks this object as white, that is unreachable (so far). */
|
||||
makeWhite(): void {
|
||||
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.WHITE;
|
||||
}
|
||||
|
||||
/** Tests if this object is black, that is reachable. Root objects are always reachable. */
|
||||
get isBlack(): bool {
|
||||
return (this.nextWithFlags & Flags.MASK) == Flags.BLACK;
|
||||
}
|
||||
|
||||
/** Marks this object as black, that is reachable. */
|
||||
makeBlack(): void {
|
||||
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.BLACK;
|
||||
}
|
||||
|
||||
/** Tests if this object is gray, that is reachable with unscanned children. */
|
||||
get isGray(): bool {
|
||||
return (this.nextWithFlags & Flags.MASK) == Flags.GRAY;
|
||||
}
|
||||
|
||||
/** Marks this object as gray, that is reachable with unscanned children. */
|
||||
makeGray(): void {
|
||||
if (this != iter) {
|
||||
this.remove();
|
||||
to.insert(this);
|
||||
} else {
|
||||
iter = iter.prev;
|
||||
}
|
||||
this.nextWithFlags = (this.nextWithFlags & ~Flags.MASK) | Flags.GRAY;
|
||||
}
|
||||
}
|
||||
|
||||
/** Collector states. */
|
||||
const enum State {
|
||||
/** Not yet initialized. */
|
||||
INIT = 0,
|
||||
/** Currently transitioning from SWEEP to MARK state. */
|
||||
IDLE = 1,
|
||||
/** Currently marking reachable objects. */
|
||||
MARK = 2,
|
||||
/** Currently sweeping unreachable objects. */
|
||||
SWEEP = 3
|
||||
}
|
||||
|
||||
/** Current collector state. */
|
||||
var state = State.INIT;
|
||||
|
||||
// From and to spaces
|
||||
var from: ManagedObject;
|
||||
var to: ManagedObject;
|
||||
var iter: ManagedObject;
|
||||
|
||||
/** Performs a single step according to the current state. */
|
||||
function gc_step(): void {
|
||||
var obj: ManagedObject;
|
||||
switch (state) {
|
||||
case State.INIT: {
|
||||
from = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE));
|
||||
from.nextWithFlags = changetype<usize>(from);
|
||||
from.prev = from;
|
||||
to = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE));
|
||||
to.nextWithFlags = changetype<usize>(to);
|
||||
to.prev = to;
|
||||
iter = to;
|
||||
// fall-through
|
||||
}
|
||||
case State.IDLE: {
|
||||
state = State.MARK;
|
||||
break;
|
||||
}
|
||||
case State.MARK: {
|
||||
obj = iter.next;
|
||||
if (obj != to) {
|
||||
iter = obj;
|
||||
obj.makeBlack();
|
||||
obj.visitFn(changetype<usize>(obj) + ManagedObject.SIZE);
|
||||
} else {
|
||||
obj = iter.next;
|
||||
if (obj == to) {
|
||||
let temp = from;
|
||||
from = to;
|
||||
to = temp;
|
||||
Flags.WHITE ^= 1;
|
||||
Flags.BLACK ^= 1;
|
||||
iter = from.next;
|
||||
state = State.SWEEP;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State.SWEEP: {
|
||||
obj = iter;
|
||||
if (obj != to) {
|
||||
iter = obj.next;
|
||||
free_memory(changetype<usize>(obj));
|
||||
} else {
|
||||
to.nextWithFlags = changetype<usize>(to);
|
||||
to.prev = to;
|
||||
state = State.IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Allocates a managed object. */
|
||||
@global
|
||||
export function gc_allocate(
|
||||
size: usize,
|
||||
visitFn: (obj: usize) => void
|
||||
): usize {
|
||||
assert(size <= MAX_SIZE_32 - ManagedObject.SIZE);
|
||||
var obj = changetype<ManagedObject>(allocate_memory(ManagedObject.SIZE + size));
|
||||
obj.makeWhite();
|
||||
obj.visitFn = visitFn;
|
||||
from.insert(obj);
|
||||
return changetype<usize>(obj) + ManagedObject.SIZE;
|
||||
}
|
||||
|
||||
/** Visits a reachable object. Called from the visitFn functions. */
|
||||
@global
|
||||
export function gc_visit(obj: ManagedObject): void {
|
||||
if (state == State.SWEEP) return;
|
||||
if (obj.isWhite) obj.makeGray();
|
||||
}
|
||||
|
||||
/** Registers a managed child object with its parent object. */
|
||||
@global
|
||||
export function gc_register(parent: ManagedObject, child: ManagedObject): void {
|
||||
if (parent.isBlack && child.isWhite) parent.makeGray();
|
||||
}
|
||||
|
||||
/** Iterates the root set. Provided by the compiler according to the program. */
|
||||
@global
|
||||
export declare function gc_roots(): void;
|
||||
|
||||
/** Performs a full garbage collection cycle. */
|
||||
@global
|
||||
export function gc_collect(): void {
|
||||
// begin collecting if not yet collecting
|
||||
switch (state) {
|
||||
case State.INIT:
|
||||
case State.IDLE: gc_step();
|
||||
}
|
||||
// finish the cycle
|
||||
while (state != State.IDLE) gc_step();
|
||||
}
|
||||
|
||||
declare function allocate_memory(size: usize): usize;
|
||||
declare function free_memory(ptr: usize): void;
|
||||
|
||||
// Considerations
|
||||
//
|
||||
// - An API that consists mostly of just replacing `allocate_memory` would be ideal, possibly taking
|
||||
// any additional number of parameters that are necessary, like the parent and the visitor.
|
||||
//
|
||||
// - Not having to generate a helper function for iterating globals but instead marking specific
|
||||
// nodes as roots could simplify the embedding, but whether this is feasible or not depends on its
|
||||
// performance characteristics and the possibility of tracking root status accross assignments.
|
||||
// For example, root status could be implemented as some sort of referenced-by-globals counting
|
||||
// and a dedicated list of root objects.
|
||||
//
|
||||
// - In 32-bit specifically, there is some free space in TLSF object headers due to alignment that
|
||||
// could be repurposed to store some GC information, like a class id. Certainly, this somewhat
|
||||
// depends on the efficiency of the used mechanism to detect this at compile time, including when
|
||||
// a different allocator is used.
|
||||
//
|
||||
// - Think about generations.
|
@ -2,8 +2,5 @@
|
||||
"extends": "../assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./collector/*.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user