-
Notifications
You must be signed in to change notification settings - Fork 11
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
Shared structs vs Serializable/Transferable objects #8
Comments
IIUC there's nothing shared as part of a That doesn't seem like an adequate replacement to me as the only sharing primitive. Having to create per-thread wrappers, even if those wrappers were easier to set up than plain objects, is a big performance issue for large object graphs. Additionally, do you have to Efficient transferables is something I've thought about in the past, and my conclusion was that it's better solved in a separate proposal. This proposal is narrowly scoped and is about providing a necessary evil/escape hatch kind of low-level shared memory primitive. I want it to be expressive enough to write lock-free code in userland, for instance, and I want it to be usable for mega-apps or frameworks willing to take on maintenance burden of something that's harder to use (but still way easier than SABs!) for the performance and memory savings. But seeing I think this is an escape hatch-y power feature, I wouldn't recommend it as something apps without strict performance or memory requirements to adopt willy nilly. For those kind of apps, something like better transferables or a coarse-grained reader-writer lock kind of model without data races is a better answer. That's a different design space, and one I plan to tackle in the future. Most importantly though, I contend that we need both this proposal and the safer, higher-level thing. |
The example doesn't use it because there was already a lot going on, but we'd want to include sugar around fields so that they can be set more directly, i.e. in the example we'd want something shorter like: shared #aborted = false; With this sugar, the object wrapper would become more of a fiction, in particular suppose we had something like the following: // All fields are shared, so when transfering to another thread
// the object wrapper is entirely fictional, the memory can be accessed
// more directly
transferable struct class PureSharedStruct {
shared x = 10;
shared y = 30;
}
// Becuase not all fields are shared on this MixedStruct there would
// be two layers to this object, the shared common
transferable struct class MixedStruct {
// Accessing these fields would essentially be direct shared memory access
shared x = 10;
shared y = 20;
// The presence of a non-shared field means there's a virtual wrapper
// object around the shared parts, however this is still kind've fiction
// because depending on what data is here the implementation may well be able
// to optimize it away with things like copy-on-write, or if we had some way to mark this
// as non-writable then even just memcpy or something
regularData = someStructuredSerializable;
}
// Here we have a pure-shared-struct like object, but with methods
// upon transfering the first of these object onto another thread
// we clone the prototype and class definition, however the rest of
// the object is effectively just shared memory, and copying instances
// beyond the first should still be ideally efficient
transferable struct class SharedStructWithMethods {
shared x = 10;
shared y = 20;
updateX() {
this.x = 30;
}
}
So the main goal with this idea is no, you don't need to post each object individually, rather the object is cloned basically as is, regular properties get structured cloned. With the sugar syntax though, engines would be easily able to discover fields that can be shared directly, i.e. these fields aren't structured cloned at all (or at least they are cloned in the same sense "SharedArrayBuffer" is "cloned" onto another theead). Now mutable object graphs would still work, we would simply require that the only kinds of values that can be stored in a And even though the machinery seems like it would be more expensive than the current proposal, as long as engines check what is defined, it would still be pay for what you use, i.e. if you have a transferable that consists purely of In fact even pure-shared + methods would still be able to be made very efficient as the prototype/class only needs to "cloned" once into the other thread ever, and even then it could be computed lazily as the idea above ensures a structured class that doesn't use |
I don't understand this part. SAB wrapper objects are actually cloned onto another thread. A core value add of the shared structs proposal is that there is no wrapper object cloning, the instance is shared directly. Is identity of your transferable structs preserved in a roundtrip? |
I seem to recall that one of the reasons for
Yeah, my intention was that it should, although I think my idea above as described doesn't quite work how I intended, there's a couple points above that would need tweaking so that everything works so that is the case. In particular about when non-shared fields get structured cloned would need to be considered properly so that round-tripping works as expected, in fact thinking about it it would probably be better that field values are not implictly structured cloned by default but we provide a way to initialize those fields properly. Existing web objects don't survive round trips, but again this is something that probably came from the same reason I'll have to have a think about exactly what tweaks would need to be made to my idea above to make it workable, but a core principle of my idea is that trivial objects without methods and without non-shared fields should work identically to how // Engines should be able to optimize this exactly the same way as
// shared struct, additional methods, accessors, non-shared fields and all that
// would be pay-for-what-you-use
transferable struct class MyClass {
shared x = 10;
shared y = 10;
} The main divergence from the current proposal is that shared fields need to be explictly declared rather than implictly applying to all fields within the "shared struct", i.e.: shared struct class MyClass {
// Implictly shared and updatable on all threads
x = 10;
}
transferable struct class MyClass {
// Explictly declared as shared, however the semantics are
// very similar to the current proposal
shared x = 10;
// The main difference is we can have non-shared fields they aren't automagically shared,
// if you don't use these fields you pay nothing
// And thinking about this more thoroughly based on your feedback I actually
// think there should be NO implicit structured cloning of this field, instead
// when the receiving thread calls `constructor()`/`super()` we'd reinitialize
// this field in the new thread as if it were a regular property
// in this regard shared fields would be special in that calling `super()` would NOT
// reinitialize those fields
y = someObject;
} |
Also just as a quick note, I think if we had // If we ship shared-struct like this, we can add non-shared fields and serialization
// methods and all that on top of this base at a later date as extensions
shared struct class MyClass {
shared x;
shared y;
} I'll have a larger think later about what this layering might look like, but as a very initial idea: shared struct class MyClass {
// This field is shared, when constructor() is invoked
// on the new thread this field is NOT INITIALIZED
shared #sharedField = 20;
// The FIRST TIME a thread receives a shared struct object it will
// cause the constructor to run, these intializers will be initialized
// in the usual way locally on that thread
#nonShared;
#nonSharedWithInitializer = new Map();
constructor() {
super();
// We'd have some way to receive cloned data in the constructor
console.log(new.clonedData);
}
// New special function-like block that allows sending some serialized data
serialize {
return structuredSerializableData;
}
} Effectively we'd have The main thing I overlooked above, and that I will need to think heavily about is when exactly the const sharedObject = new SharedObject();
transferToAnotherThread(sharedObject);
const otherSharedObject = new SharedObject();
// We can't atomically call the constructor on the other thread,
// however we might be able to schedule it to be called when it is
// first "observed" on the other thread, i.e.
// when `sharedObject.sharedField` is invoked on the other thread, if
// the object has a constructor and isn't already initialized then
// it will trigger the constructor, this will require some careful thought
// though to ensure it behaves nicely
sharedObject.sharedField = otherSharedObject; |
So after some reflection, I think that while the concept I had above might be able to made technically feasible, it is probably still less appealing for both the shared structs use cases and the custom serializable/transferable use cases and the two use cases are probably better served by this proposal and this issue/proposal separately. (In fact the latter would probably be able to utilize shared structs fairly effectively for certain serializable objects). |
This is an issue covering quite a large idea, however I think it would be considerably more ergonomic that the much lower level shared struct idea, while still perfectly permitting high efficiency shared structs where no code sharing is used.
So the idea is as the title suggests, instead of having highly basic "shared structs" we expose the notion of serializable/transferable as a first-class concept within the JS language itself. This would allow authors to implement a rich class of objects rather than the rather painful status quo of marshalling such objects into lower-level serializable/transferable values.
As an example consider a host object like
OffscreenCanvas
returned from a call to.transferControlToOffscreen()
, such an object will have an implicit shared memory buffer which the thread can write with theOffscreenCanvas
abstraction, but the renderer can read from an entirely different thread.Now to give an example of how such an API could potentially look, I give below an example of a theoretical version of
AbortSignal
that is transferable, involves shared state, but is otherwise compatible with the existing API:The text was updated successfully, but these errors were encountered: