Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalidate inline caches on method mutation #130

Merged
merged 4 commits into from
Jun 5, 2020

Conversation

ltratt
Copy link
Member

@ltratt ltratt commented Jun 4, 2020

This fixes the problem identified in SOM-st/SOM#35 and which, AFAIK, affects all SOM implementations: mutating a method in a class causes incorrect behaviour if an inline cache exists for that call (e.g. yksom (before this PR) and Java SOM could "ignore" the change if a cache had been created; and C SOM apparently crashes!). This PR fixes the problem for yksom.

It does so in a brute force way: when a method in any class is changed, every inline cache in the VM is reset. This makes correctness easy to reason about, though clearly it is less efficient than more nuanced approaches.

The way we do this is sort-of interesting from a Rust perspective. There are two obvious ways to model this:

  • Have every SOM Array have a bool that says "if I'm mutated, flush all inline caches".
  • Subclass the Array struct (in Rust) so that only instances of the MethodArray subclass flush inline caches on mutation.

The former is ugly, the latter seems impossible: Rust doesn't have subclasses, and traits only partially model them. In this case, the bool would probably be most efficient, but it's just really ugly. So I tried to make the latter case work, even though it doesn't appear to be possible. In particular, and perhaps surprisingly (though there are good reasons for this), a trait object T1 + T2 cannot be coerced into a trait object of either T1 or T2. This PR uses a simple way to solve this problem:

  1. We define a trait Array
  2. We define two structs NormalArray (which is the old Array struct renamed) and MethodsArray (which is only used for the arrays of methods in Classes).
  3. We add a to_array function to Obj which returns (simplified) &dyn Array. Given a &dyn Obj we can then turn it into a &dyn Array by calling to_array if and only if an implementation of to_array is provided. Only NormalArray and MethodsArray implement this.

Using this, we can get from a Val to a &dyn Array by calling (simplified) val.tobj().to_array(). Yes, it means we have two dynamically dispatched function calls, but it means that we have a relatively nice way of separating out the behaviour of normal arrays from method arrays.

Most of the PR is thus shuffling code around (and duplicating some code): the real meat is in 0889b12.

This will allow us to introduce an Array trait in the next commit.

mutate_methods = (
run = (
| g_idx h_idx ms |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will not stop to point out that changing coding style amounts to changing the language...

Smalltalks use camelCase for variables, method names, ...
They use PascalCase for class names.

Inconsistency has a non-zero cost.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone makes a PR to change the whole lot in one go, I'll happily change style to match other SOM's conventions henceforth. Until then, I'm just going to carry on being consistent internally within yksom.

let store = unsafe { &mut *self.store.get() };
if idx > 0 && idx <= store.len() {
idx -= 1;
*unsafe { store.get_unchecked_mut(idx) } = val;
vm.flush_inline_caches();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A cheap trick could be if (old != new) flushCache.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but this is such a crude mechanism (and old == new an unlikely outcome) that I'm not sure it's worth the complexity. When we eventually JIT this VM I hope to use a much more fine-grained mechanism, but it feels like overkill in a plain interpreters!

@@ -29,6 +29,10 @@ impl Obj for NormalArray {
vm.array_cls
}

fn to_array(&self) -> Result<&dyn Array, Box<VMError>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this return a Result. I don't see how this can cause an error here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to implement with this return type because it's defined in the trait.

@ptersilie
Copy link
Member

There's one (possibly two) typos in the commit messages:

Everything else looks fine. So feel free to force-push.

For the time being, this is functionally indistinguishable from a NormalArray:
they both respond to the same SOM methods. However, we will soon use the
MethodsArray struct to solve the problem of cache invalidation.

The way this works is as follows. Given a `Val`, one can now (via a `ThinObj`)
get a reference to the object as an `Array` trait object via the `to_array`
function in the `Obj` trait. [This slightly annoying dance is necessary because
one cannot otherwise coerce one trait object to another in Rust. By introducing
this method, and having the structs `impl` it, we use normal (and safe!) Rust
mechanisms to achieve the effect we want.]
yksom's use of inline caches (like most/all other SOMs) is beneficial to
performance, but leads to a correctness problem when a program mutates the
array resulting from the "methods" function call. mutate_methods.som in Java
SOM, for example, prints #b #b as if the method had not been mutated. Before
this commit, yksom also printed the same.

This commit makes use of MethodsArray to flush every inline cache in the VM
whenever a method array is mutated. By flushing every inline cache, we easily
solve the problem of changes to a superclass's methods being reflected in
subclasses. This is a sledgehammer to crack a nut, but it is a complex nut, and
rarely encountered. More elegant solutions are possible.
@ltratt
Copy link
Member Author

ltratt commented Jun 5, 2020

to do solve: 1a86c05

Fixed.

#b #b: 0889b12

That one is correct (it's showing a bug in Java SOM).

I've forced pushed an update.

@ptersilie
Copy link
Member

bors r+

@bors
Copy link
Contributor

bors bot commented Jun 5, 2020

Build succeeded:

@bors bors bot merged commit 30769b4 into softdevteam:master Jun 5, 2020
@ltratt ltratt deleted the invalidate_caches branch June 6, 2020 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants