Skip to content

Commit

Permalink
Add better support for native threads (#124)
Browse files Browse the repository at this point in the history
* better support for native threads

* dont create allocations for ephemeral heap in deserialization

* add benchmark for thread creation

* add github workflow for pushing docs
  • Loading branch information
mattwparas authored Dec 22, 2023
1 parent a79f9aa commit 7bdf828
Show file tree
Hide file tree
Showing 11 changed files with 771 additions and 260 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Deploy
on:
push:
branches:
- master

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Deploy GitHub Pages
run: |
cd docs
mdbook build
git worktree add gh-pages
git config user.name "Deploy from CI"
git config user.email ""
cd gh-pages
# There are benchmarks stored in dev/bench/ that we would like to
# keep around. Otherwise, this just adds the book as a separate
# directory.
rm -rf book
mv ../book/ .
git add .
git commit -m "Deploy $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages
28 changes: 27 additions & 1 deletion crates/steel-core/benches/my_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ fn fib_28(c: &mut Criterion) {
let mut vm = Engine::new();
// vm.compile_and_run_raw_program(PRELUDE).unwrap();
vm.compile_and_run_raw_program(
"(define (fib n) (#%black-box) (if (<= n 2) 1 (+ (fib (- n 1)) (fib (- n 2)))))",
"(define (fib n) (if (<= n 2) 1 (+ (fib (- n 1)) (fib (- n 2)))))",
)
.unwrap();

Expand All @@ -147,6 +147,31 @@ fn fib_28(c: &mut Criterion) {
group.finish();
}

fn thread_creation(c: &mut Criterion) {
let mut vm = Engine::new();
vm.compile_and_run_raw_program(
r#"
(define (foo x)
(vector 10 20 30 40 x))
(define (block)
(thread-join! (spawn-thread! (lambda () (vector-ref (foo 100) 4)))))
"#,
)
.unwrap();

let script = "(block)";
let program = vm.emit_raw_program_no_path(script).unwrap();
let executable = vm.raw_program_to_executable(program).unwrap();

let mut group = c.benchmark_group("thread-creation");
// group.sample_size(200);
group.bench_function("thread-creation", |b| {
b.iter(|| vm.run_executable(&executable))
});
group.finish();
}

fn fib_28_contract(c: &mut Criterion) {
let mut vm = Engine::new();
vm.compile_and_run_raw_program(PRELUDE).unwrap();
Expand Down Expand Up @@ -292,6 +317,7 @@ criterion_group!(
ten_thousand_iterations_letrec,
trie_sort,
fib_28,
thread_creation,
engine_creation,
register_function,
multiple_transducers,
Expand Down
10 changes: 6 additions & 4 deletions crates/steel-core/src/compiler/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,16 @@ impl ConstantMap {
SerializableConstantMap(self.to_bytes().unwrap())
}

pub fn to_serializable_vec(&self) -> Vec<SerializableSteelVal> {
pub fn to_serializable_vec(
&self,
serializer: &mut std::collections::HashMap<usize, SerializableSteelVal>,
visited: &mut std::collections::HashSet<usize>,
) -> Vec<SerializableSteelVal> {
self.values
.borrow()
.iter()
.cloned()
.map(into_serializable_value)
.map(|x| into_serializable_value(x, serializer, visited))
.collect::<Result<_>>()
.unwrap()
}
Expand All @@ -73,8 +77,6 @@ impl ConstantMap {
// }

pub fn from_vec(vec: Vec<SteelVal>) -> ConstantMap {
// ConstantMap(Rc::new(RefCell::new(vec)))

ConstantMap {
map: Rc::new(RefCell::new(
vec.clone()
Expand Down
188 changes: 169 additions & 19 deletions crates/steel-core/src/rvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ use crate::{
tokens::TokenType,
},
rerrs::{ErrorKind, SteelErr},
steel_vm::vm::{BuiltInSignature, Continuation},
steel_vm::vm::{threads::closure_into_serializable, BuiltInSignature, Continuation},
values::port::SteelPort,
values::{
closed::{HeapRef, MarkAndSweepContext},
closed::{Heap, HeapRef, MarkAndSweepContext},
functions::ByteCodeLambda,
lazy_stream::LazyStream,
port::SendablePort,
structs::SerializableUserDefinedStruct,
transducers::{Reducer, Transducer},
},
Expand Down Expand Up @@ -808,77 +809,185 @@ pub enum SerializableSteelVal {
Void,
StringV(String),
FuncV(FunctionSignature),
MutFunc(MutFunctionSignature),
HashMapV(Vec<(SerializableSteelVal, SerializableSteelVal)>),
// If the value
ListV(Vec<SerializableSteelVal>),
VectorV(Vec<SerializableSteelVal>),
BoxedDynFunction(BoxedDynFunction),
BuiltIn(BuiltInSignature),
SymbolV(String),
Custom(Box<dyn CustomType + Send>), // Custom()
Custom(Box<dyn CustomType + Send>),
CustomStruct(SerializableUserDefinedStruct),
// Attempt to reuse the storage if possible
HeapAllocated(usize),

Port(SendablePort),
}

pub enum SerializedHeapRef {
Serialized(Option<SerializableSteelVal>),
Closed(HeapRef<SteelVal>),
}

pub struct HeapSerializer<'a> {
pub heap: &'a mut Heap,
pub fake_heap: &'a mut std::collections::HashMap<usize, SerializedHeapRef>,
// After the conversion, we go back through, and patch the values from the fake heap
// in to each of the values listed here - otherwise, we'll miss cycles
pub values_to_fill_in: &'a mut std::collections::HashMap<usize, HeapRef<SteelVal>>,

// Cache the functions that get built
pub built_functions: &'a mut std::collections::HashMap<usize, Gc<ByteCodeLambda>>,
}

// Once crossed over the line, convert BACK into a SteelVal
// This should be infallible.
pub fn from_serializable_value(val: SerializableSteelVal) -> SteelVal {
pub fn from_serializable_value(ctx: &mut HeapSerializer, val: SerializableSteelVal) -> SteelVal {
match val {
SerializableSteelVal::Closure(c) => SteelVal::Closure(Gc::new(c.into())),
SerializableSteelVal::Closure(c) => {
if c.captures.is_empty() {
if let Some(already_made) = ctx.built_functions.get(&c.id) {
SteelVal::Closure(already_made.clone())
} else {
let id = c.id;
let value = Gc::new(ByteCodeLambda::from_serialized(ctx, c));

// Save those as well
// Probably need to just do this for all
ctx.built_functions.insert(id, value.clone());
SteelVal::Closure(value)
}
} else {
SteelVal::Closure(Gc::new(ByteCodeLambda::from_serialized(ctx, c)))
}
}
SerializableSteelVal::BoolV(b) => SteelVal::BoolV(b),
SerializableSteelVal::NumV(n) => SteelVal::NumV(n),
SerializableSteelVal::IntV(i) => SteelVal::IntV(i),
SerializableSteelVal::CharV(c) => SteelVal::CharV(c),
SerializableSteelVal::Void => SteelVal::Void,
SerializableSteelVal::StringV(s) => SteelVal::StringV(s.into()),
SerializableSteelVal::FuncV(f) => SteelVal::FuncV(f),
SerializableSteelVal::MutFunc(f) => SteelVal::MutFunc(f),
SerializableSteelVal::HashMapV(h) => SteelVal::HashMapV(
Gc::new(
h.into_iter()
.map(|(k, v)| (from_serializable_value(k), from_serializable_value(v)))
.map(|(k, v)| {
(
from_serializable_value(ctx, k),
from_serializable_value(ctx, v),
)
})
.collect::<HashMap<_, _>>(),
)
.into(),
),
SerializableSteelVal::VectorV(v) => {
SteelVal::ListV(v.into_iter().map(from_serializable_value).collect())
}
SerializableSteelVal::ListV(v) => SteelVal::ListV(
v.into_iter()
.map(|x| from_serializable_value(ctx, x))
.collect(),
),
SerializableSteelVal::VectorV(v) => SteelVal::VectorV(SteelVector(Gc::new(
v.into_iter()
.map(|x| from_serializable_value(ctx, x))
.collect(),
))),
SerializableSteelVal::BoxedDynFunction(f) => SteelVal::BoxedFunction(Rc::new(f)),
SerializableSteelVal::BuiltIn(f) => SteelVal::BuiltIn(f),
SerializableSteelVal::SymbolV(s) => SteelVal::SymbolV(s.into()),
SerializableSteelVal::Custom(b) => SteelVal::Custom(Gc::new(RefCell::new(b))),
SerializableSteelVal::CustomStruct(s) => {
SteelVal::CustomStruct(Gc::new(UserDefinedStruct {
fields: s.fields.into_iter().map(from_serializable_value).collect(),
fields: s
.fields
.into_iter()
.map(|x| from_serializable_value(ctx, x))
.collect(),
type_descriptor: s.type_descriptor,
}))
}
SerializableSteelVal::Port(p) => SteelVal::PortV(SteelPort::from_sendable_port(p)),
SerializableSteelVal::HeapAllocated(v) => {
// todo!()

if let Some(mut guard) = ctx.fake_heap.get_mut(&v) {
match &mut guard {
SerializedHeapRef::Serialized(value) => {
let value = std::mem::take(value);

if let Some(value) = value {
let value = from_serializable_value(ctx, value);
let allocation = ctx.heap.allocate_without_collection(value);

ctx.fake_heap
.insert(v, SerializedHeapRef::Closed(allocation.clone()));

SteelVal::HeapAllocated(allocation)
} else {
// println!("If we're getting here - it means the value from the heap has already
// been converting. if so, we should do something...");

let fake_allocation =
ctx.heap.allocate_without_collection(SteelVal::Void);

ctx.values_to_fill_in.insert(v, fake_allocation.clone());

SteelVal::HeapAllocated(fake_allocation)
}
}

SerializedHeapRef::Closed(c) => SteelVal::HeapAllocated(c.clone()),
}
} else {
// Shouldn't silently fail here, but we will... for now

let allocation = ctx.heap.allocate_without_collection(SteelVal::Void);

ctx.fake_heap
.insert(v, SerializedHeapRef::Closed(allocation.clone()));

SteelVal::HeapAllocated(allocation)
}
}
}
}

pub fn into_serializable_value(val: SteelVal) -> Result<SerializableSteelVal> {
// The serializable value needs to refer to the original heap -
// that way can reference the original stuff easily.

// TODO: Use the cycle detector instead
pub fn into_serializable_value(
val: SteelVal,
serialized_heap: &mut std::collections::HashMap<usize, SerializableSteelVal>,
visited: &mut std::collections::HashSet<usize>,
) -> Result<SerializableSteelVal> {
// dbg!(&serialized_heap);

match val {
SteelVal::Closure(c) => Ok(SerializableSteelVal::Closure(c.unwrap().try_into()?)),
SteelVal::Closure(c) => closure_into_serializable(&c, serialized_heap, visited)
.map(SerializableSteelVal::Closure),
SteelVal::BoolV(b) => Ok(SerializableSteelVal::BoolV(b)),
SteelVal::NumV(n) => Ok(SerializableSteelVal::NumV(n)),
SteelVal::IntV(n) => Ok(SerializableSteelVal::IntV(n)),
SteelVal::CharV(c) => Ok(SerializableSteelVal::CharV(c)),
SteelVal::Void => Ok(SerializableSteelVal::Void),
SteelVal::StringV(s) => Ok(SerializableSteelVal::StringV(s.to_string())),
SteelVal::FuncV(f) => Ok(SerializableSteelVal::FuncV(f)),
SteelVal::ListV(l) => Ok(SerializableSteelVal::VectorV(
SteelVal::ListV(l) => Ok(SerializableSteelVal::ListV(
l.into_iter()
.map(into_serializable_value)
.map(|x| into_serializable_value(x, serialized_heap, visited))
.collect::<Result<_>>()?,
)),
SteelVal::BoxedFunction(f) => Ok(SerializableSteelVal::BoxedDynFunction((*f).clone())),
SteelVal::BuiltIn(f) => Ok(SerializableSteelVal::BuiltIn(f)),
SteelVal::SymbolV(s) => Ok(SerializableSteelVal::SymbolV(s.to_string())),

SteelVal::MutFunc(f) => Ok(SerializableSteelVal::MutFunc(f)),
SteelVal::HashMapV(v) => Ok(SerializableSteelVal::HashMapV(
v.0.unwrap()
.into_iter()
.map(|(k, v)| {
let kprime = into_serializable_value(k)?;
let vprime = into_serializable_value(v)?;
let kprime = into_serializable_value(k, serialized_heap, visited)?;
let vprime = into_serializable_value(v, serialized_heap, visited)?;

Ok((kprime, vprime))
})
Expand All @@ -899,11 +1008,52 @@ pub fn into_serializable_value(val: SteelVal) -> Result<SerializableSteelVal> {
.fields
.iter()
.cloned()
.map(into_serializable_value)
.map(|x| into_serializable_value(x, serialized_heap, visited))
.collect::<Result<Vec<_>>>()?,
type_descriptor: s.type_descriptor,
},
)),

SteelVal::PortV(p) => SendablePort::from_port(p).map(SerializableSteelVal::Port),

// If there is a cycle, this could cause problems?
SteelVal::HeapAllocated(h) => {
// We should pick it up on the way back the recursion
if visited.contains(&h.as_ptr_usize())
&& !serialized_heap.contains_key(&h.as_ptr_usize())
{
// println!("Already visited: {}", h.as_ptr_usize());

Ok(SerializableSteelVal::HeapAllocated(h.as_ptr_usize()))
} else {
visited.insert(h.as_ptr_usize());

if serialized_heap.contains_key(&h.as_ptr_usize()) {
// println!("Already exists in map: {}", h.as_ptr_usize());

Ok(SerializableSteelVal::HeapAllocated(h.as_ptr_usize()))
} else {
// println!("Trying to insert: {} @ {}", h.get(), h.as_ptr_usize());

let value = into_serializable_value(h.get(), serialized_heap, visited);

let value = match value {
Ok(v) => v,
Err(e) => {
// println!("{}", e);
return Err(e);
}
};

serialized_heap.insert(h.as_ptr_usize(), value);

// println!("Inserting: {}", h.as_ptr_usize());

Ok(SerializableSteelVal::HeapAllocated(h.as_ptr_usize()))
}
}
}

illegal => stop!(Generic => "Type not allowed to be moved across threads!: {}", illegal),
}
}
Expand Down
Loading

0 comments on commit 7bdf828

Please sign in to comment.