Enabling run-time exclusivity checking in release mode

Enabling run-time exclusivity checking in release mode

It's time to change the compiler default for run-time exclusivity
checks. This will expose the feature to more testing and allow the
compiler team to gather performance feedback from users. Until now,
I've been waiting until the most obvious bottlenecks are
resolved--there's no sense having a slew of performance bugs for
issues that are about to be fixed. Recent optimizations landed by Joe
Shajrawi have brought performance where I think it needs to be for
adoption. More optimizations are planned, and some benchmarks should
be further improved, but at this point we're ready to begin receiving
bug reports. That will help prioritize the remaining work for Swift 5.

What's changing in Swift 5

Exclusivity checking was introduced in Swift 4.0.
SE-0176: Enforce Exclusive Access to Memory.

Compile-time (static) diagnostics catch many common exclusivity
violations, but run-time (dynamic) diagnostics are also required to
catch violations involving escaping closures, class properties, and
globals. Swift 4.0 provided both static and dynamic enforcement, but
dynamic enforcement was only enabled in debug builds.

In Swift 4.1 and 4.2, compiler diagnostics were gradually strengthened
to catch more and more of the cases in which programmers could skirt
exclusivity rules--most notably, by capturing variables in nonescaping
closures or by converting nonescaping closures to escaping closures.

Devin's Swift 4.2 announcement, Upgrading exclusive access warning to
be an error in Swift
4.2
,
explains some of the common cases affected by the newly enforced
exclusivity diagnostics.

The goal for Swift 5 is to fix the remaining holes in the language
model and to fully enforce that model. All of the known language
deficiencies have been fixed, with the exception of [SR-8546]: Enforce
@escaping for nested functions named inside conditional
expressions
. Run-time enforcement will be enabled for release builds by
default in PR 20302.

This change could impact Swift programs that previously appeared
well-behaved, but weren't fully tested in debug mode. Now, when running
in release mode, they may trap with the message "error: overlapping
accesses...".

Why we need exclusivity checks.

A combination of static and dynamic checks are necessary to enforce
Swift's language rules for a particular aspect of Memory
Safety
. Enforcing the rules helps in at least four ways:

  1. Adherence to exclusivity rules removes a common class of
    programming bugs involving mutable state and action at a distance.

As programs scale in size, it becomes increasingly likely for routines
to interact in unexpected ways. Exclusivity rules eliminate dangerous
interactions involving mutable state. In this simple example, note
that exclusivity prevents the programmer from passing the same
instance of Names as src and dest, which would otherwise cause
an infinite loop:

func moveElements(from src: inout Set<String>, to dest: inout Set<String>) {
  while let e = src.popFirst() {
    dest.insert(e)
  }
}

class Names {
  var nameSet: Set<String> = []
}

func moveNames(from src: Names, to dest: Names) {
  moveElements(from: &src.nameSet, to: &dest.nameSet)
}
  1. Enforcement eliminates an unspecified behavior rule from the language.

Prior to Swift 4, these memory rules were unenforced, making it the
programmer's responsibility to adhere to them. In practice, it was
easy for programmers to violate these rules in subtle ways, leaving
their programs vulnerable to unspecified behavior.

  1. Enforcement legalizes performance optimization while protecting
    memory safety.

A guarantee of exclusivity on inout parameters and mutating
methods provides important information to the compiler, which it can
use to optimize memory access and reference counting
operations. Declaring an unspecified behavior rule, as mentioned
above, does not provide this guarantee. An "undefined behavior" rule
is required to support optimization, but such a rule would, by
definition, compromise the memory safety of the language. Full
exclusivity enforcement allows the compiler to optimize based on
memory exclusivity without introducing undefined behavior.

  1. Exclusivity rules are needed to give programmer control of
    ownership and move-only types.

John McCall's Ownership Manifesto intoduces the Law of
Exclusivity
,
and explains how it provides the basis for adding ownership and move-only types
to the language.

Current performance impact

Of the 656 public microbenchmarks in the Swift repository, there are
still several regressions larger than 10%:

TEST                    OLD      NEW      DELTA      RATIO
ClassArrayGetter2       139      1307     +840.3%    **0.11x** 
HashTest                631      1233     +95.4%     **0.51x** 
NopDeinit               21269    32389    +52.3%     **0.66x** 
Hanoi                   1478     2166     +46.5%     **0.68x** 
Calculator              127      158      +24.4%     **0.80x** 
Dictionary3OfObjects    391      455      +16.4%     **0.86x** 
CSVParsingAltIndices2   526      604      +14.8%     **0.87x** 
Prims                   549      626      +14.0%     **0.88x** 
CSVParsingAlt2          1252     1411     +12.7%     **0.89x** 
Dictionary4OfObjects    206      232      +12.6%     **0.89x** 
ArrayInClass            46       51       +10.9%     **0.90x** 

The common pattern in these benchmarks is to define an array of data
as a class property and to repeatedly access that array through the
class reference. Each of those class property accesses now incurs a
runtime call. Naturally, introducing a runtime call in a loop that
otherwise does almost no work incurs substantial overhead. This is
similar to the issue caused by automatic reference counting. In some
cases, more sophistacated optimization will be able to determine the
same object is repeatedly accessed. Furthermore, the overhead of the
runtime call can be improved. But regardless of how well we optimize,
there will always a class of microbenchmarks in which the runtime
check has a noticeable impact.

As a general guideline, avoid performing class property access within
the most performance critical loops, particularly on different objects
in each loop iteration. If that isn't possible, it may help if the
visibility of those class properties is private or internal.

20 Likes

Awesome!

Local inout bindings (as discussed in the ownership manifesto) might be helpful as a way to easily optimize repeated class-property accesses that may need to mutate.

2 Likes

Absent this language feature, you can also use a closure to perform multiple operations as a single mutating access, for example:

class Foo {
  var bar: [Int] = []
}

let foo = Foo()

_ = { (bar: inout [Int]) in
  bar.append(1)
  bar.append(2)
  bar.append(3)
}(&foo.bar)

Great work @Andrew_Trick!

2 Likes
Terms of Service

Privacy Policy

Cookie Policy