Skip to content

Copy-on-write representation in SIL: instructions and builtins #31728

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

Merged
merged 4 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 148 additions & 2 deletions docs/SIL.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,94 @@ make the use of such types more convenient; it does not shift the
ultimate responsibility for assuring the safety of unsafe
language/library features away from the user.

Copy-on-Write Representation
----------------------------

Copy-on-Write (COW) data structures are implemented by a reference to an object
which is copied on mutation in case it's not uniquely referenced.

A COW mutation sequence in SIL typically looks like::

(%uniq, %buffer) = begin_cow_mutation %immutable_buffer : $BufferClass
cond_br %uniq, bb_uniq, bb_not_unique
bb_uniq:
br bb_mutate(%buffer : $BufferClass)
bb_not_unique:
%copied_buffer = apply %copy_buffer_function(%buffer) : ...
br bb_mutate(%copied_buffer : $BufferClass)
bb_mutate(%mutable_buffer : $BufferClass):
%field = ref_element_addr %mutable_buffer : $BufferClass, #BufferClass.Field
store %value to %field : $ValueType
%new_immutable_buffer = end_cow_mutation %buffer : $BufferClass

Loading from a COW data structure looks like::

%field1 = ref_element_addr [immutable] %immutable_buffer : $BufferClass, #BufferClass.Field
%value1 = load %field1 : $*FieldType
...
%field2 = ref_element_addr [immutable] %immutable_buffer : $BufferClass, #BufferClass.Field
%value2 = load %field2 : $*FieldType

The ``immutable`` attribute means that loading values from ``ref_element_addr``
and ``ref_tail_addr`` instructions, which have the *same* operand, are
equivalent.
In other words, it's guaranteed that a buffer's properties are not mutated
between two ``ref_element/tail_addr [immutable]`` as long as they have the
same buffer reference as operand.
This is even true if e.g. the buffer 'escapes' to an unknown function.


In the example above, ``%value2`` is equal to ``%value1`` because the operand
of both ``ref_element_addr`` instructions is the same ``%immutable_buffer``.
Conceptually, the content of a COW buffer object can be seen as part of
the same *static* (immutable) SSA value as the buffer reference.

The lifetime of a COW value is strictly separated into *mutable* and
*immutable* regions by ``begin_cow_mutation`` and
``end_cow_mutation`` instructions::

%b1 = alloc_ref $BufferClass
// The buffer %b1 is mutable
%b2 = end_cow_mutation %b1 : $BufferClass
// The buffer %b2 is immutable
(%u1, %b3) = begin_cow_mutation %b1 : $BufferClass
// The buffer %b3 is mutable
%b4 = end_cow_mutation %b3 : $BufferClass
// The buffer %b4 is immutable
...

Both, ``begin_cow_mutation`` and ``end_cow_mutation``, consume their operand
and return the new buffer as an *owned* value.
The ``begin_cow_mutation`` will compile down to a uniqueness check and
``end_cow_mutation`` will compile to a no-op.

Although the physical pointer value of the returned buffer reference is the
same as the operand, it's important to generate a *new* buffer reference in
SIL. It prevents the optimizer from moving buffer accesses from a *mutable* into
a *immutable* region and vice versa.

Because the buffer *content* is conceptually part of the
buffer *reference* SSA value, there must be a new buffer reference every time
the buffer content is mutated.

To illustrate this, let's look at an example, where a COW value is mutated in
a loop. As with a scalar SSA value, also mutating a COW buffer will enforce a
phi-argument in the loop header block (for simplicity the code for copying a
non-unique buffer is not shown)::

header_block(%b_phi : $BufferClass):
(%u, %b_mutate) = begin_cow_mutation %b_phi : $BufferClass
// Store something to %b_mutate
%b_immutable = end_cow_mutation %b_mutate : $BufferClass
cond_br %loop_cond, exit_block, backedge_block
backedge_block:
br header_block(b_immutable : $BufferClass)
exit_block:

Two adjacent ``begin_cow_mutation`` and ``end_cow_mutation`` instructions
don't need to be in the same function.


Instruction Set
---------------

Expand Down Expand Up @@ -3199,6 +3287,56 @@ strong reference count is greater than 1.
A discussion of the semantics can be found here:
:ref:`arcopts.is_unique`.

begin_cow_mutation
``````````````````

::

sil-instruction ::= 'begin_cow_mutation' '[native]'? sil-operand

(%1, %2) = begin_cow_mutation %0 : $C
// $C must be a reference-counted type
// %1 will be of type Builtin.Int1
// %2 will be of type C

Checks whether %0 is a unique reference to a memory object. Returns 1 in the
first result if the strong reference count is 1, and 0 if the strong reference
count is greater than 1.

Returns the reference operand in the second result. The returned reference can
be used to mutate the object. Technically, the returned reference is the same
as the operand. But it's important that optimizations see the result as a
different SSA value than the operand. This is important to ensure the
correctness of ``ref_element_addr [immutable]``.

The operand is consumed and the second result is returned as owned.

The optional ``native`` attribute specifies that the operand has native Swift
reference counting.

end_cow_mutation
````````````````

::

sil-instruction ::= 'end_cow_mutation' '[keep_unique]'? sil-operand

%1 = end_cow_mutation %0 : $C
// $C must be a reference-counted type
// %1 will be of type C

Marks the end of the mutation of a reference counted object.
Returns the reference operand. Technically, the returned reference is the same
as the operand. But it's important that optimizations see the result as a
different SSA value than the operand. This is important to ensure the
correctness of ``ref_element_addr [immutable]``.

The operand is consumed and the result is returned as owned. The result is
guaranteed to be uniquely referenced.

The optional ``keep_unique`` attribute indicates that the optimizer must not
replace this reference with a not uniquely reference object.

is_escaping_closure
```````````````````

Expand Down Expand Up @@ -4193,7 +4331,7 @@ ref_element_addr
````````````````
::

sil-instruction ::= 'ref_element_addr' sil-operand ',' sil-decl-ref
sil-instruction ::= 'ref_element_addr' '[immutable]'? sil-operand ',' sil-decl-ref

%1 = ref_element_addr %0 : $C, #C.field
// %0 must be a value of class type $C
Expand All @@ -4205,11 +4343,15 @@ Given an instance of a class, derives the address of a physical instance
variable inside the instance. It is undefined behavior if the class value
is null.

The ``immutable`` attribute specifies that all loads of the same instance
variable from the same class reference operand are guaranteed to yield the
same value.

ref_tail_addr
`````````````
::

sil-instruction ::= 'ref_tail_addr' sil-operand ',' sil-type
sil-instruction ::= 'ref_tail_addr' '[immutable]'? sil-operand ',' sil-type

%1 = ref_tail_addr %0 : $C, $E
// %0 must be a value of class type $C with tail-allocated elements $E
Expand All @@ -4222,6 +4364,10 @@ object which is created by an ``alloc_ref`` with ``tail_elems``.
It is undefined behavior if the class instance does not have tail-allocated
arrays or if the element-types do not match.

The ``immutable`` attribute specifies that all loads of the same instance
variable from the same class reference operand are guaranteed to yield the
same value.

Enums
~~~~~

Expand Down
29 changes: 29 additions & 0 deletions include/swift/AST/Builtins.def
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,29 @@ BUILTIN_SIL_OPERATION(IsUnique, "isUnique", Special)
/// BridgeObject to be treated as a native object by the runtime.
BUILTIN_SIL_OPERATION(IsUnique_native, "isUnique_native", Special)

/// beginCOWMutation<T : AnyObject>(inout T) -> Int1
///
/// Begins a copy-on-write mutation for a buffer reference which is passed as
/// inout argument. It returns a true if the buffer is uniquely referenced.
/// In this case the buffer may be mutated after calling this builtin.
///
/// The beginCOWMutation builtin is very similar to isUnique. It just translates
/// to a different SIL instruction (begin_cow_mutation), which is the preferred
/// representation of COW in SIL.
BUILTIN_SIL_OPERATION(BeginCOWMutation, "beginCOWMutation", Special)

/// beginCOWMutation_native<T : AnyObject>(inout T) -> Int1
///
/// Like beginCOWMutation, but it's assumed that T has native Swift reference
/// counting.
BUILTIN_SIL_OPERATION(BeginCOWMutation_native, "beginCOWMutation_native", Special)

/// endCOWMutation<T : AnyObject>(inout T)
///
/// Ends a copy-on-write mutation for a buffer reference which is passed as
/// inout argument. After calling this builtin, the buffer must not be mutated.
BUILTIN_SIL_OPERATION(EndCOWMutation, "endCOWMutation", Special)

/// bindMemory : <T> (Builtin.RawPointer, Builtin.Word, T.Type) -> ()
BUILTIN_SIL_OPERATION(BindMemory, "bindMemory", Special)

Expand Down Expand Up @@ -651,6 +674,12 @@ BUILTIN_MISC_OPERATION(AssignCopyArrayFrontToBack, "assignCopyArrayFrontToBack",
BUILTIN_MISC_OPERATION(AssignCopyArrayBackToFront, "assignCopyArrayBackToFront", "", Special)
BUILTIN_MISC_OPERATION(AssignTakeArray, "assignTakeArray", "", Special)

/// COWBufferForReading has type <T: AnyObject> T -> T
///
/// Returns the buffer reference which is passed as argument.
/// This builtin indicates to the optimizer that the buffer is not mutable.
BUILTIN_MISC_OPERATION(COWBufferForReading, "COWBufferForReading", "n", Special)

// unsafeGuaranteed has type <T: AnyObject> T -> (T, Builtin.Int8)
BUILTIN_MISC_OPERATION(UnsafeGuaranteed, "unsafeGuaranteed", "", Special)

Expand Down
21 changes: 17 additions & 4 deletions include/swift/SIL/SILBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -1422,9 +1422,10 @@ class SILBuilder {
}

RefElementAddrInst *createRefElementAddr(SILLocation Loc, SILValue Operand,
VarDecl *Field, SILType ResultTy) {
VarDecl *Field, SILType ResultTy,
bool IsImmutable = false) {
return insert(new (getModule()) RefElementAddrInst(
getSILDebugLocation(Loc), Operand, Field, ResultTy));
getSILDebugLocation(Loc), Operand, Field, ResultTy, IsImmutable));
}
RefElementAddrInst *createRefElementAddr(SILLocation Loc, SILValue Operand,
VarDecl *Field) {
Expand All @@ -1434,9 +1435,10 @@ class SILBuilder {
}

RefTailAddrInst *createRefTailAddr(SILLocation Loc, SILValue Ref,
SILType ResultTy) {
SILType ResultTy,
bool IsImmutable = false) {
return insert(new (getModule()) RefTailAddrInst(getSILDebugLocation(Loc),
Ref, ResultTy));
Ref, ResultTy, IsImmutable));
}

DestructureStructInst *createDestructureStruct(SILLocation Loc,
Expand Down Expand Up @@ -1722,6 +1724,17 @@ class SILBuilder {
return insert(new (getModule()) IsUniqueInst(getSILDebugLocation(Loc),
operand, Int1Ty));
}
BeginCOWMutationInst *createBeginCOWMutation(SILLocation Loc,
SILValue operand, bool isNative) {
auto Int1Ty = SILType::getBuiltinIntegerType(1, getASTContext());
return insert(BeginCOWMutationInst::create(getSILDebugLocation(Loc), operand,
Int1Ty, getFunction(), isNative));
}
EndCOWMutationInst *createEndCOWMutation(SILLocation Loc, SILValue operand,
bool keepUnique = false) {
return insert(new (getModule()) EndCOWMutationInst(getSILDebugLocation(Loc),
operand, keepUnique));
}
IsEscapingClosureInst *createIsEscapingClosure(SILLocation Loc,
SILValue operand,
unsigned VerificationType) {
Expand Down
21 changes: 18 additions & 3 deletions include/swift/SIL/SILCloner.h
Original file line number Diff line number Diff line change
Expand Up @@ -1966,8 +1966,8 @@ SILCloner<ImplClass>::visitRefElementAddrInst(RefElementAddrInst *Inst) {
getBuilder().setCurrentDebugScope(getOpScope(Inst->getDebugScope()));
recordClonedInstruction(
Inst, getBuilder().createRefElementAddr(
getOpLocation(Inst->getLoc()), getOpValue(Inst->getOperand()),
Inst->getField(), getOpType(Inst->getType())));
getOpLocation(Inst->getLoc()), getOpValue(Inst->getOperand()),
Inst->getField(), getOpType(Inst->getType()), Inst->isImmutable()));
}

template<typename ImplClass>
Expand All @@ -1977,7 +1977,8 @@ SILCloner<ImplClass>::visitRefTailAddrInst(RefTailAddrInst *Inst) {
recordClonedInstruction(
Inst, getBuilder().createRefTailAddr(getOpLocation(Inst->getLoc()),
getOpValue(Inst->getOperand()),
getOpType(Inst->getType())));
getOpType(Inst->getType()),
Inst->isImmutable()));
}

template <typename ImplClass>
Expand Down Expand Up @@ -2370,6 +2371,20 @@ void SILCloner<ImplClass>::visitIsUniqueInst(IsUniqueInst *Inst) {
Inst, getBuilder().createIsUnique(getOpLocation(Inst->getLoc()),
getOpValue(Inst->getOperand())));
}
template<typename ImplClass>
void SILCloner<ImplClass>::visitBeginCOWMutationInst(BeginCOWMutationInst *Inst) {
getBuilder().setCurrentDebugScope(getOpScope(Inst->getDebugScope()));
recordClonedInstruction(
Inst, getBuilder().createBeginCOWMutation(getOpLocation(Inst->getLoc()),
getOpValue(Inst->getOperand()), Inst->isNative()));
}
template<typename ImplClass>
void SILCloner<ImplClass>::visitEndCOWMutationInst(EndCOWMutationInst *Inst) {
getBuilder().setCurrentDebugScope(getOpScope(Inst->getDebugScope()));
recordClonedInstruction(
Inst, getBuilder().createEndCOWMutation(getOpLocation(Inst->getLoc()),
getOpValue(Inst->getOperand()), Inst->doKeepUnique()));
}
template <typename ImplClass>
void SILCloner<ImplClass>::visitIsEscapingClosureInst(
IsEscapingClosureInst *Inst) {
Expand Down
Loading