> On Nov 1, 2016, at 11:00 AM, Jordan Rose via swift-dev <swift-dev@swift.org> 
> wrote:
> 
> - Does this help us with the nested dictionary CoW problem? 
> `foo["bar"]["baz"] += 1`

My understanding is that this problem arises because we don’t have ‘optional 
addressors’. A dictionary lookup might return nil. If addressors had a way to 
return an optional wrapping a pointer, and optional evaluation supported this, 
we could do in-place updates of this kind. For Apple people: I have a radar 
that I think describes this problem (rdar://17509619).

Slava

> 
> Jordan
> 
> 
> 
>> On Oct 31, 2016, at 12:22, Joe Groff via swift-dev <swift-dev@swift.org 
>> <mailto:swift-dev@swift.org>> wrote:
>> 
>> We currently abstract over mutable property accesses using what I’ll call a 
>> continuation-based model–the materializeForSet accessor is called before an 
>> inout access, and returns a continuation callback that must be called when 
>> the inout access is completed. I think a nested-function-based model, if 
>> designed correctly, has the potential to be more efficient and easier to 
>> implement.
>> 
>> As background for anyone intimately familiar with the implementation, 
>> Swift’s language model for mutable properties requires a reconciliation step 
>> after a mutation occurs. For example, in an inout access like this:
>> 
>> foo(&x.y.z)
>> the y or z property may be computed, requiring a getter call before the call 
>> to foo and a setter call afterward, or either property may have willSet or 
>> didSet observers that fire after foo finishes. If the base x is a class, 
>> protocol, or generic value, the implementation of y can`t be statically 
>> known, so we need an abstract interface for mutating the property, 
>> projecting out the value at the start of the access and committing the 
>> mutated value back at the end. Ideally, the interface should accommodate 
>> efficient access to both stored and computed properties, avoiding 
>> unnecessary copying of stored properties to avoid inducing CoW copies or, in 
>> the near future, violating the constraints of move-only types or the borrow 
>> model. There are two broad approaches to doing this:
>> 
>> Continuation-based access
>> 
>> The accessor that begins the formal access can return a continuation closure 
>> to call when the access is completed. This is the approach Swift currently 
>> uses with its materializeForSet accessor pattern. A property under this 
>> model compiles to something like this C code:
>> 
>> struct X {
>>   // getter for Y
>>   Y (*getY)(const X *self);
>>   // setter for Y
>>   void (*setY)(X *self, Y newValue);
>>   // materializeForSet for Y
>>   struct MaterializeForSetReturn {
>>     // The projected field
>>     Y *y;
>>     // The function that finishes the access, or null
>>     void (*finish)(X *self, void *buffer);
>>   }; 
>>   struct MaterializeForSetReturn (*materalizeForSettingY)(X *self, void 
>> *buffer);
>> };
>> 
>> void callFooOnXDotYDotZ(X *x) {
>>   // To compile:
>>   //   foo(&x.y.z)
>>   // 
>>   // - call the materializeForSet accessor to begin the access to y, giving 
>> it a buffer
>>   //   to store a computed value or capture state to pass to the end of the 
>> access
>>   char alignas(Y) bufferY[sizeof(Y) + 3*sizeof(void*)];
>>   auto materializedY = x->materializeForSettingY(x, bufferY);
>>   // - call materializeForSet to begin access to z
>>   char alignas(Z) bufferZ[sizeof(Z) + 3*sizeof(void*)];
>>   autom materializedZ = 
>> materializedY.y->materializeForSettingZ(materializedY.y, bufferZ);
>>   // - perform the access
>>   foo(materializedZ.z);
>>   // - finish the accesses, if we were given a finishing callback
>>   if (materializedZ.finish)
>>     materializedZ.finish(materializedY.y, bufferZ);
>>   if (materializedY.finish)
>>     materializedY.finish(x, bufferY);
>> }
>> A stored property can be exposed through this interface by trivially 
>> returning the projected address of the subobject, with no continuation:
>> 
>> struct MaterializeForSetReturn
>> materializeForSettingStoredY(X *self, void *buffer) {
>>   return (struct MaterializeForSetReturn){&self->y, NULL};
>> }
>> whereas a computed property can call the getter, store the computed value 
>> into the buffer, and return a continuation function that calls the setter:
>> 
>> struct MaterializeForSetReturn
>> materializeForSettingComputedY(X *self, void *buffer) {
>>   Y oldValue = getY(self);
>>   memcpy(buffer, &oldValue, sizeof(Y));
>>   return (struct MaterializeForSetReturn){(Y *)buffer, 
>> finishSettingComputedY};
>> }
>> void finishSettingComputedY(X *self, void *buffer) {
>>   Y newValue;
>>   memcpy(&newValue, buffer, sizeof(Y));
>>   setY(self, newValue);
>> }
>> A benefit of this approach is that it maintains the integrity of the source 
>> function in the compiled code. On the other hand, in order for the property 
>> implementation to transfer state from the start to the completion of the 
>> access, the caller must preallocate some stack space; if it’s too much, the 
>> space is wasted, and if it’s not enough, the property implementation must 
>> use malloc to get more space for itself. (It’s theoretically possible to 
>> avoid this with some clever stack pointer accounting.) There’s also a code 
>> size impact on the caller, which needs to bracket the inout access with a 
>> call on each side. The underlying CPU has to execute three or five branches 
>> per segment of the access path (calling and returning from 
>> materializeForSet, testing for null, and potentially calling and returning 
>> from the continuation if there is one).
>> 
>> Nested functions
>> 
>> Alternatively, we can implement the formal access pattern as a series of 
>> nested functions. A property under this model compiles to something like 
>> this C code:
>> 
>> struct X {
>>   // getter for Y
>>   Y (*getY)(const X *self);
>>   // setter for Y
>>   void (*setY)(X *self, Y newValue);
>>   // mutator for Y
>>   void *(*mutateY)(X *self, void *context, void *(*innerMutation)(Y *y, void 
>> *context));
>> };
>> 
>> void callFooOnXDotYDotZ(X *x) {
>>   // To compile:
>>   //   foo(&x.y)
>>   // - call the mutate accessor for y, with the inner access as a
>>   //   function whose pointer gets passed in
>>   x->mutateY(x, NULL, callFooOnXDotY_inner_1);
>> }
>> void callFooOnXDotYDotZ_inner_1(Y *y, void *context) {
>>   // - call the mutate accessor for z
>>   y->mutateZ(y, context, callFooOnXDotY_inner_2);
>> }
>> void callFooOnXDotYDotZ_inner_2(Z *z, void *context) {
>>   foo(z);
>> }
>> A stored property can implement mutate by projecting the physical address of 
>> the subobject, tail-calling the subsequent inner function:
>> 
>> void *mutateStoredY(X *self, void *context, void *(*innerMutation)(Y *y, 
>> void *context)) {
>>   /*tail*/ return innerMutation(&self->y, context);
>> }
>> and a computed property can bracket the inner mutation in getter and setter 
>> calls:
>> 
>> void *mutateComputedY(X *self, void *context, void *(*innerMutation)(Y *y, 
>> void *context)) {
>>   Y value = getY(self);
>>   void *result = innerMutation(&value, context);
>>   setY(self, value);
>>   return result;
>> }
>> This makes things easier for the property implementation, which can easily 
>> save as much state as it needs to on the stack instead of relying on a 
>> preallocated buffer handed down from the caller. There’s one fewer branch 
>> necessary per segment of the access path compared to the continuation-based 
>> model, either three if the accessor has no reconciliation necessary and can 
>> tail-call the inner function, or four if it does a normal call. On the other 
>> hand, the caller function has to be broken up into many smaller 
>> subfunctions, potentially one for every segment of an access path.
>> 
>> Letting nested functions walk the access path
>> 
>> We can reduce the number of nested functions that are needed for a nested 
>> access path by designing a convention that allows each mutator to directly 
>> call the mutator for the next segment of access. For example, the caller 
>> could construct the entire access path as an array of pointers to mutation 
>> functions:
>> 
>> typedef void *(*MutatorFunction)(void *base, MutatorFunction *accessPath, 
>> void *context);
>> void callFooOnXDotYDotZDotW(X *x) {
>>   // foo(&x.y.z.w)
>>   MutatorFunction accessPath[] = {
>>     (MutatorFunction)mutateZ,
>>     (MutatorFunction)mutateW,
>>     (MutatorFunction)callFooOnXDotYDotZDotW_inner
>>   };
>>   mutateY(x, accessPath, NULL);
>> }
>> Each property accessor function then loads the next step of the access out 
>> of the path, and advances the pointer forward for the nested access:
>> 
>> void *mutateY(X *self, MutatorFunction *accessPath, void *context) {
>>   // Project out y
>>   Y *y = self->y;
>>   // Call the next step of the access path, passing the remainder of the 
>> access path
>>   return (*accessPath)(y, accessPath + 1, context);
>> }
>> And the inner function at the end of the access path ignores the access path 
>> parameter and performs the access:
>> 
>> void *callFooOnXDotYDotZDotW_inner(W *w, MutatorFunction *_ignored, void 
>> *context) {
>>   foo(w);
>> }
>> In this model, only one inner function needs to be broken out of the caller 
>> for the innermost inout access, at least for simple property projections. 
>> There are only two branches needed per segment, or one for segments that can 
>> tail-call the inner access. Parameterized segments such as subscripts can be 
>> supported by storing the parameters in the access path buffer as well, and 
>> having the subscript accessor advance past the parameters when it consumes 
>> them. For example:
>> 
>> void callFooOnXDotYSubIDotZ(X *x, intptr_t i) {
>>   // foo(&x.y[i].z)
>>   uintptr_t accessPath[] = {
>>     (uintptr_t)mutateYSubscript,
>>     (uintptr_t)i,
>>     (uintptr_t)mutateZ,
>>     (uintptr_t)callFooOnXDotYSubIDotZ_inner
>>   };
>>   
>>   mutateY(x, accessPath, NULL);
>> }
>> 
>> void *mutateYSubscript(Y *y, uintptr_t *accessPath, void *context) {
>>   // Get the subscript index out of the access path
>>   intptr_t i = (intptr_t)*accessPath++;
>>   // Get the next step of the access out of the access path
>>   MutatorFunction next = (MutatorFunction)*accessPath++;
>>   
>>   return next(&y->elements[i], accessPath, context);
>> }
>> On the other hand, this does push the inner access functions and subscript 
>> arguments onto the stack, which is a small amount of overhead. Furthermore, 
>> the caller function has to push any state it needs to pass from outside the 
>> access to inside on the stack as well, if it isn’t able to cram it otherwise 
>> into a single context pointer argument.
>> 
>> Machine-level calling convention tricks
>> 
>> To be able to feed a return value from the inner access back to the outer 
>> caller, nested mutators would need to preserve all of the return registers 
>> potentially used by the inner access on the way out of the access. To 
>> minimize the impact on the outer caller, the convention for mutators could 
>> also specify that they need to preserve the contents of callee-save 
>> registers when they call down to nested accesses. This should allow as much 
>> state to be kept in registers between the outer caller and inner access as 
>> if they were naturally in the same function, with at most a function call in 
>> between.
>> 
>> With these sorts of optimizations, I think the nested function approach has 
>> the potential to be competitive with, or even more efficient than, our 
>> current continuation-based materializeForSet design.
>> 
>> -Joe
>> _______________________________________________
>> swift-dev mailing list
>> swift-dev@swift.org <mailto:swift-dev@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-dev
> 
> _______________________________________________
> swift-dev mailing list
> swift-dev@swift.org
> https://lists.swift.org/mailman/listinfo/swift-dev

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Reply via email to