Transcript
[ Applause ]
>> Good afternoon.
My name is Phillipe Hausler, and
I'm here today, with my
colleague, Donna Tom.
And we're going to talk about
Efficient Interaction with
Frameworks.
Now, we all care very deeply
about performance.
We want our laptops to be fast.
We want our phones and tablets
to have all day battery life.
And we want to do amazing things
with our desktops.
As a matter of fact, all of
these devices should have great
performance.
And it's our jobs to be able to
make that happen.
Now, performance has many
dimensions.
How fast code can run, how much
power it takes.
Or what the memory footprint of
that is.
Since, there are multiple
dimensions, how can we visualize
this a little bit better?
To give you a framework, no pun
intended, for visualizing this.
You can think of it as a graph
with the size of the data that
you're working with on one axis
and the frequency on another.
If you're looking at a lot of
data and code that runs very
frequently, you're going to be
up there in that first quadrant.
And these are going to be things
that are going to be likely to
be able to make a notable impact
upon performance.
And you're going to want to
spend time optimizing these
cases.
But if you're looking at
something that works with just a
little bit of data and runs just
a few times.
You're going to be down there in
that third quadrant.
And to be honest, you really
don't want to spend a whole lot
of time worrying about them, too
much.
It's that second and fourth
quadrant that are a bit
trickier.
These are gray areas that are
highly dependent upon the
situation.
And in these cases, you'll most
certainly want to be able to
measure the performance in a
scenario that reflects actual
usage.
And then, use that information
to be able to evaluate whether
it's worth your time to be able
to make changes.
In this release, we took a deep
look to be able to understand
how we could make performance
better across the operating
system.
But for your apps, as well.
We will go over a few really
awesome changes that we made in
Foundation.
And of course, Swift has been a
big part of this release, as we
took a long hard look at how
bridging works for some of the
Foundation types.
To be able to make them faster
and work better in your
applications.
Now, strings are, of course, are
a huge part of many apps.
They're used as tokens, human
readable data formats, and
displaying to the screen.
And efficient string handling
makes a big impact on many
applications.
And often is a huge part of the
critical content being displayed
to users.
But of course, the reason why
you are here, is you want to
make your app run faster.
You want to use less energy.
You want to get more things done
with less RAM.
And don't worry, we'll get to
that on each of the sections
that we're going to be talking,
today, about.
To give you things to keep in
mind when optimizing for
performance.
Now, as I said, we made a number
of performance improvements
across the operating system.
And in Foundation, we made some
pretty nice changes.
These are just a few of the
highlights.
We overhauled NSCalendar for
date enumeration, not only to
use less memory, but also, it's
much faster, too.
And trust me when I say,
calendrical calculations are
really tricky to get right.
And then, this update, the
NSCalendar implementation is not
only faster, but also, corrects
some outstanding Edge cases that
have been lurking around for a
while, now.
But when making changes, you
have to consider the scale at
which that change will impact.
And in Foundation and Core
Foundation, we took a number of
places where the small things
that would add up.
And we looked at a deep dive of
how thread-safe operations in
Foundation work.
And decided to migrate to using
Atomics and OS and Fairlock,
which in the end, ends up
playing a lot better with
quality of service.
Now, speaking of quality of
service, NSOperation and
OperationQueue have been also
overhauled to have more correct
implementation, whenever it
comes to their quality of
service.
You'll see some pretty neat
performance improvements, as
well.
And in heavy cases, we've seen
up to 25% improvement on
queueing operations, just to
point out one highlight.
And after working for a while,
now, in Swift, we realized that
copy on write is pretty
fantastic.
And in Foundation, a number of
the collection types will now
use copy on write as their
backing storage.
So, what's this whole copy on
write thing?
Copy on write is a mechanism, or
COW for short, where two items
can point to a shared backing
store until a mutation occurs.
And when that mutation happens,
the mutating party copies that
backing storage to be able to
allow for the write to happen.
So, in short, copying isn't
costly, anymore.
This means that whenever you
defensively copy a mutable
container, it's almost free.
And before, copies of
collections were at best, linear
execution time.
And now, whenever you copy them,
they're constant until a shared
mutation.
So, let's pull that apart a
little bit and understand how
it's working, under the hood.
So, in this particular example,
we're creating a new mutable
array.
And when this happens, we
initially have a COW backing
store that holds zero items.
So, we do some extra work and in
our application, we then call
copy.
In this particular case, we're
assigning B as a copy of A.
And whenever that copy has
occurred, the only price that
you pay in your application is
the allocation of the new
collection.
You don't have to actually copy
any of the items.
So, in this case, we're still
pointing to the same backing
store that holds zero items.
So, later on, if we were to make
a mutation, then what will
happen is that the copying party
initially has a reference to
that shared backing store.
So, in order to make a mutation,
it has to copy from that backing
store to be able to make sure
that the mutation is in safe.
But you have to take in
consideration that most
applications are going to be
ending at that point, right
there, whenever you don't have
any further mutations.
So, as you can see, you can end
up having a vast performance
improvement by leveraging this
feature.
Now, let's see how you can
actually use this in your
application.
Now, I'm as guilty as the next
developer.
I've written code like this with
comments, with the hopes that my
colleagues are going to follow
my suggestion, all in the name
of performance.
But there's a little pitfall,
here that if a mutable array
were to have snuck in, then we
would end up sharing mutable
state.
Which means we are going to be
sharing bugs.
Nobody wants that.
But since copies are nearly
free, now, we can do the same
thing every single time and not
have to worry about the
performance hit.
It's pretty great.
But it isn't just the copy
nature of properties.
Many times, mutable containers
are used to build things up.
And in this sample, the author
knew that NSMutableArray is a
subclass of an NSArray.
And the advertised return value
is an NSArray.
So, it's mostly safe, right?
Well, unfortunately, there's
some other consequences that can
happen, here.
If somebody checks the actual
class of the return type, well,
oops.
They could end up having a
mutation of shared state, again.
So instead, you can defensively
copy return values so that it
does the right thing without
having to worry about the
performance costs.
There's actually another case
that's a little bit more hidden.
In the case of Swift, whenever
either of these two APIs were
exported, we have to make a copy
to be able to preserve value
types.
And so, if you cast to an array
of any, from the NSArray for
either of these two APIs, the
previous implementation would
have to spend a linear execution
time to be able to make that
copy.
If you defensively do so, the
copy then, doesn't end up
attributing to some other place
that is unknown performance
costs.
In Swift 3, we introduced a
number of structural types for
Foundation.
One that made a whole lot of
sense was NSData being bridged
to the structural type data.
And we took a long look at data
to be able to understand common
use cases and places that we
could improve data to be able to
make it work better in our
applications.
And in this release, we've got
now data is its own slice type.
And we've looked at the
performance for being able to do
common tasks like for example,
getting the Count of the data.
Or indexing to a specific byte
at an offset.
And some of these
implementations are kind of
extreme.
Normally a few machine
instructions wouldn't move the
needle, at all.
But what it comes to
representing a byte buffer, even
20 versus four instructions can
actually make a difference.
So, this code looks pretty
simple.
But if has a few interesting
characteristics that reveal some
insight on how we could make
data faster.
First off, data is a collection,
just like array.
It can be subscripted, but both
indexes and ranges.
So, this means that the start
index of the data, is not always
zero.
Because the index is similar to
iterators in the other
languages.
And for the record, this code
has nothing wrong with it.
We just used it to be able to
understand what parts of data
should be refined.
So, the two questions, here, are
how big is the data that we're
dealing with?
And how many times it's called?
Where is it on that chart?
And the answer truly is that it
could fall on almost any of
them.
And most likely, it's in that
place where it needs to be
measured.
So, we did exactly that.
And on the top in the blue, is
the initial Swift 3 version of
Data.
And when subscripting, it took
about 16 nanoseconds on the
machine that I was using to be
able to profile it.
And since data is a common
currency of dealing with a
collection of bytes, this should
be really, really fast.
After tuning, we got it just
down to four nanoseconds.
It's pretty impressive.
And if you were using Data,
before, you get this advantage
for free.
And it will also be able to
interoperate with all the rest
of the APIs that take and use
data.
Thanks.
[ Applause ]
Now again, preface, here for
you, none of these examples are
clearly wrong or harmful.
But they do have things to be
able to consider versus their
counterparts.
Oftentimes, it's viewed that a
collection of bytes can be
expressed in an array.
And in small cases, sure.
That works just fine.
However, there's a hidden cost,
here, from a cognitive sense
that when you try to write it to
a file, that' pretty tricky.
There's a lot of Edge cases,
there.
We take care of that for you.
Being able to interoperate with
things like writing to files,
converting to basic C4, Data is
a clear winner.
Now, sometimes we get nostalgic
and we fall back to old trusty
malloc.
And unfortunately, this can
sometimes skip out on other
optimizations.
Like for example, knowing proper
allocation sizes that work best
with rounding to the right
regions that malloc returns for
buffers.
Data deals with all of this, for
you.
So, you don't have to worry
about reallocation.
You don't have to worry about
understanding the Edge cases of
what sizes might be better for
malloc, than others.
Now, these two next lines are
really similar.
And they're worth noting,
because in certain cases, you
want to be able to work with a
large region of data.
And in other cases, you want to
be able to just hold around a
small little bit of it.
So, Data has two APIs.
One of which is subdata in
range.
And this will create a copy, so
that if you have a large file
that you're working with, but
only want to keep around a small
bit.
Subdata with range will make an
enforced copy, like that.
But we changed Data so that it's
its own subtype, or sub-slice of
a type.
So, that whenever you use the
range syntax, even for example,
this version of ranges.
You can use that as a window to
be able to peer into the data.
So, if you have a large file
that you're dealing with, that
you just need to be able to
parse it and the data itself
will only be transitory.
Then, using slices is an
incredibly efficient manner to
be able to access it, because it
causes no copy.
Now, we've talked a number of
times about bridges.
And there are two types of
bridges that are relevant, here.
And on the left we have
toll-free bridging.
And in these cases, they are
bridging from a Foundation type
to a Core Foundation type.
Or from a Foundation type to a
Core Foundation type.
And these are zero cost at the
cast.
So, whatever you actually have
in this particular case, the
NSArray being bridged to a
CFArray, it's just a
reinterpretation of a pointer.
But there is a slight cost to be
paid, whenever you pass that
object to CFArrayGetCount,
there's something that you have,
there.
We'll go over that, here in just
a little bit.
So, contrasting that to Swift
bridging, those are cases where
you're bridging from a reference
type to a structural type.
And contrarily so, bridging from
a structural type to a reference
type.
But the costs in these
particular cases are paid in
advance.
So, whenever you import from
Objective-C or if, in this case,
you use as question mark, to be
able to convert between the two,
you are paying that cost, there.
But the differential is that
these are then normal costs at
the usage.
So, let's dive in a little bit
more.
I know this looks scary.
Don't have a cow, actually, as a
matter of fact, this doesn't
actually.
CFArray doesn't implement copy
on write.
But whenever you pass an NSArray
or subclass into
CFArrayGetCount, it will
magically call out to this
Objective-C method count.
So, let's pick it apart a little
bit further and understand how
this is different than the Swift
bridge.
Here it is, a little bit more
simplified.
First, we tested the arrays and
Objective-C subclass.
And then, we see that invocation
to count, there.
If it wasn't, we know that the
structural layout of the object
can let us indirect to that
variable count.
So, expanding it a bit further.
It checks the internal layout of
the object against the expected
class table.
So, in truth it basically boils
down to costing two indirections
and a function call, to
determine if the Objective-C
method needs to actually be
invoked or not.
So, let's wrap it up for this
one.
Casting to an array or subclass
to a CFArray is just a
reinterpretation of the pointer.
It's the usage points that
actually have the cost.
So, this is usually an
exceedingly small impact.
But in rare cases, it could
often move the needle a little
bit.
So, on the graph, we're mostly
down there in that third
quadrant, maybe peeking up a
little bit.
Now, on the flipside, we have
Swift bridging.
Now, remember this is whenever
we call as question mark or
expose an API from Objective-C,
whenever we have a bridged
reference type.
The compiler will emit this
bridgeable family of functions,
in which will in turn in this
particular case, invoke the
referencing initializer for
Data.
And when Data is initialized
with a reference it will make a
copy to store into the backing
storage of the data.
Because we need to be able to
preserve, not only the value
type nature of the data.
But also, make sure that we
aren't holding onto a shared
mutable reference, causing of
course, bugs.
So, you can see that if this
were actually a mutable case or
a proxy or some other subclass,
it could be a potentially costly
point under the hood.
Now, bringing it all back
together, let's look at that
graph, again.
And in this time, the bridge is
not too often hit.
And whenever the common case,
the copy is optimized into a
retain.
So, we're going to, again, be
down there in that third
quadrant.
But in the cases of the
exceptions for subclasses, like
for example, mutable data, that
copy could potentially be in any
of those quadrants.
So, if you have subclasses that
you need to be able to deal
with, or you're passing mutable
versions back and forth across
the bridge.
You should probably be
understanding those with better
measurements with useful cases.
But this same pattern occurs for
not just data, but all of the
structural types within Swift.
Things like arrays and
dictionaries and strings.
Now, speaking of strings, I
heard they're kind of popular.
Here to guide you through the
wonderful world of strings,
ranges, and texts is Donna.
[ Applause ]
>> Thanks, Phillipe.
Now, strings are probably one of
the most frequently used data
types, ever.
If you're an app developer, your
app probably creates and mutates
hundreds, if not thousands, of
strings each time they're used.
And if you're a framework
developer, your framework's
probably create and mutate
strings each time someone calls
one of our APIs.
And those strings might then be
mutated farther beyond the
boundaries of your framework.
And strings, they're not used in
a vacuum.
You're interacting with
frameworks to do anything
interesting with strings.
Whether it's slicing them,
dicing them, smashing them
together.
Or even just rendering them on
the screen.
And so, you may be able to
improve your app or framework's
performance by understanding how
strings, ranges, and text
interact with frameworks.
And by making implementation
choices based on this
understanding.
But before we dive into the
nitty gritty details, I'm going
to kind of go back to rehash
some of what Phillipe talked
about and talk a little bit
about evaluating the impact of
performance improvements.
Now, it's really important not
to lose sight of the big picture
when you're looking to improve
performance.
It's really easy to get caught
up in the details and become
really focused on optimizing for
those very particular scenarios.
But it those scenarios don't
reflect the way users actually
use your app framework, then
it's not a very efficient use of
your time to optimize for them.
And so, once you've decided that
a scenario you're looking at
reflects actual usage, you might
then look at the performance of
a particular piece of code.
And when you do that, it's
important to keep in mind, these
concepts that we've discussed.
How big is the data I'm working
with?
And how often is that code
running?
And so, we're going to bring
back the graph.
But we're going to change the
axis labels a little bit for the
context of strings and text.
So, the general concept is the
same.
But for strings, we'll think of
it in terms of how long or how
short the string is and how
frequently that code is running.
So, let's keep these concepts of
scale and frequency in mind as
we cover these topics.
First, we're going to start with
string bridging.
And then next, we'll talk about
ranges and the nuances of string
index.
And finally, we'll share a few
tips for working efficiently
with text layout and rendering
in AVKit and UIKit.
So, let's get started with sting
bridging.
The first example we'll look at
works with UILabel.
So, let's say I have a label
like this, and I want to access
its text.
In Swift, I might start with
something like this.
But we asked the UIKit framework
to give us the label's text.
So, here's what the interface to
that looks like.
But remember, that this is just
a generated interface.
UILabel is implemented in
Objective-C.
And so, even though our variable
text is a Swift string, the
backing store is actually an
NSString and it's bridged from
Objective-C.
And so, now let's take a look at
what happens when we ask for
that label's text from Swift.
The NSString form the framework
is a reference type, while
Swift's string is a value type.
And so, when we ask the
framework for that NSString,
it's wrapped in the value type
when it crosses the Swift
bridge.
But we don't know what might
happen to that original NSString
after bridging.
And so, to preserve Swift value
semantics, the framework has to
make a copy of it.
Now luckily, in this case, the
original NSString is immutable.
And so, when the framework makes
that copy, it's optimized to
just retain, which is pretty
cheap.
Since, it's just incrementing
the ref count.
But even if we did make a full
copy of this string, let's go
back to our graph and evaluate
the impact.
Now in this case, the original
string consisted of seven ASCII
characters.
So, even if we made a full copy
the impact would be pretty
small.
Now, most of the time UILabels
are going to consist of short
strings that are used for UI
display purposes.
And so, you're probably not
going to be fetching their text
very frequently.
And in most cases, you'll end up
down her in quadrant three.
So, the bridging copies aren't
going to be a big deal.
But now, let's take a look at
what happens in a larger scale
example, like in NSText storage.
NSText storage is the
fundamental storage mechanism
behind TextKit.
It's used to power text views
like the one you see here, in
both Cocoa and Cocoa Touch.
And so, if you're working with
text views, you're going to want
to be able to access the text
inside that text storage.
And so, here's what that looks
like in Swift.
Here's the generated interface.
And here's the Objective-C
interface.
But notice here, the NSText
storage is a subclass of
NSMutableAttributedString.
Now, since NSText storage is
intended for working with text
editing, it's reasonable to
expect the contents of that text
storage to be mutated,
frequently.
And the contents of the text
storage could also be a very
long string.
It could be megabytes or even
gigabytes in size.
And so, for efficiency, the
framework only keeps the mutable
string around.
So, when you ask for that string
property on the text storage,
what you'll get is going to be
backed by an NSString that
refers to the mutable string.
And so now, once again, we'll
take a look at what happens when
we ask for that string property
from Swift.
Just as before, it'll be wrapped
in the value type when it
crosses the bridge, because it's
an NSString.
And the framework is going to
make a copy.
But unlike in the UILabel case,
here the underlying NSString is
actually mutable.
So, this copy could be
expensive.
And as we said previously, text
storage is much more likely to
contain long length strings.
It could be megabytes or even
gigabytes in size, so
potentially this copy could be
very expensive.
But now, let's take a look at
what happens when we ask for the
mutable string property.
NSMutableString is a reference
type that is not bridged.
And because it's not bridged,
there's no copy.
So, we avoid the cost of the
copy, here.
This situation results from a
mismatch between Swift's value
semantics and the design of
NSText storage, which needs to
use reference semantics for
performant management of large
amounts of text.
So, we're working on solving
this problem, here at Apple.
But we don't quite have the
solution, yet.
So, you should be aware that
this is something that can
happen.
And if you're working with very
large amounts of text and the
text storage, use MutableString
to access it, even if you don't
plan on mutating it.
But before you go bananas
changing all of your string
accesses to MutableString, let's
consider that graph, again.
Now, due to the nature of the
text storage API, you're
probably going to be up here on
the top half, in terms of
frequency.
So then, the real question
becomes, how much text do you
expect that storage to contain?
A kilobyte?
Might be in here, it's not too
bad.
If you use the string property,
that's probably fine.
One megabyte?
You're starting to move into
first quadrant territory, here.
And you may want to consider
using MutableString.
One gigabyte?
I really hope you're using
MutableString.
And as I said, we're working on
fixing this on our end, here.
So, keep an eye out for it in
future releases.
And so, now that we have a
better understanding of string
bridging, let's move on to
ranges.
Now, I don't know about the rest
of you, but this is certainly
how I feel when I have to work
with ranges and string index in
Swift.
And to see why, let's consider a
string containing the face palm
emoji, which coincidentally,
looks a little bit like me.
So, here's our string.
It's a length one in terms of
perceived characters.
But this one character consists
of three individual components.
We have this jaundice face palm,
a skin tone modifier, presumable
to get rid of the jaundice, and
a gender modifier.
But these visual components
don't tell the whole story,
either.
There are also, two control
characters in this string.
A zero width joiner and a
variation selector.
And to see this, we'll look at
the Unicode Scalar values that
make up the string.
Now, if you're not familiar with
the term, a Unicode Scalar value
is a 21-bit number that uniquely
represents a Unicode character.
And so, here are the Unicode
Scalar values that make up the
string, and the names that are
associated with those values.
So, if you look at the string
from a Unicode Scalar point of
view, it's actually made up of
five different values, and it
has length five.
Now, this is all fine and dandy
if you're working purely with
Swift's string API.
But if you're using
NSAttributedString, or any API
really, that uses NSRange, these
think in terms of UTF-16.
And so, if you look at the
UTF-16 view of this string, it
actually consists of seven
values and it's of length,
seven.
Now this can get really
confusing, and it makes working
with NSRange and range of string
index a little bit painful.
So, let's clear up some of this
confusion and talk about how to
work with NSAttributedString,
which makes heavy use of
NSRange.
So, let's say I have a string
like this.
And I want to create an
attributed string with it, and
change the background color of
one of the characters to green.
Now, this is complicated enough,
that even I forget how to do it,
sometimes.
But don't tell anyone.
So, I might have to look it up
on the internet.
And then, after I do that I
might end up with some code that
looks like this.
it's annoying, because I have to
keep flipping back and forth
between this string API and this
NSString API, right.
I have to take my original
string and then, create an
NSString from it, and then
calculate the NSRange using the
NSString.
But then, I have to go back and
create my
NSMutableAttributedString using
the original string, again.
Yuck. I don't like to do this.
Nobody likes to do this.
The good news is that you won't
have to do this, anymore,
because in Swift 4, we're
introducing new initializers for
NSRange and Range.
And so, when we use these new
initializers.
Thank you.
That same example trims down to
just this and it's a lot easier
to read, write, and remember.
So, the new NSRange initializer
that's being used here, it takes
a range and the Swift string and
it uses it to create the
NSRange.
And you can just pass that
directly into the
attributedString API.
But now, let's take a look at
the conversion in the other
direction from NSRange to Range
and string index.
To look at this simple example,
let's say we have some html like
this, and we want to print out
all the start tags.
And so, in order to do this,
we'll use NSRegularExpression to
find the tags we want, and then
append them to a string.
Sounds reasonable, right?
But the NSRegularExpression API
gives me NS ranges back from my
match groups.
And I need ranges of string
index to be able to append to my
Swift string.
And so, before these new
initializers were available, we
might have use something like
this to convert from NSRange to
range a string index.
And remember that face palm
emoji we talked about a few
minutes ago, and how it was of
length seven, in terms of
UTF-16?
And length five in terms of
Unicode Scalar?
Well, this code is a little bit
complicated, because it's doing
that conversion from UTF-16 to
Unicode Scalar.
But now, with these new array
initializers, you don't have to
do that anymore, either.
We can just take the NSRange we
get back from the match group
and use that to create our range
of string index, and append it
directly to our string.
This is a lot more convenient
and it's a lot easier to use.
So, these new initializers are
really great, and I hope that
you use them for all your Range
and NSRange conversions.
[ Applause ]
Thank you.
[ Applause ]
So, that wraps up ranges.
Let's move on to text layout and
rendering.
Text is hard.
It seems simple on the surface.
Because everyone knows what text
is.
Everyone encounters it on a
daily basis.
It's familiar and it's ordinary.
And as a result, there is a
tendency to implicitly assume
that it's easy.
But it's not easy.
Text poses some real performance
challenges, because of its
ubiquity in scale.
So, think about this.
We ship 40 localizations for
iOS, 35 for macOS, 39 for
watchOS, and 40 again for tvOS.
And for all of these platforms,
we support text input for more
than 300 other languages.
And each of these languages may
have different rules for things
like word breaking and
hyphenation.
And that's going to affect the
line breaking, which is going to
affect the text layout, which is
going to affect the text
rendering.
And our frameworks need to be
able to handle all of these
languages, correctly.
Now, if that wasn't enough, here
are just some of the other
factors that our frameworks take
into account when we perform
text layout and rendering.
So, we look at all of these
things and more to render your
text in a way that's both
correct and performant.
So, I encourage you to use the
standard label controls whenever
possible.
Now, with so many different
variables to consider, our
frameworks use multiple
optimization strategies, under
the hood.
Selection of any one particular
strategy is highly situational
and multiple criteria might have
to be met in order to apply one.
And so, I would warn you to be
careful when you're applying
your own optimizations on top of
the standard controls.
Because a change in rendering
conditions or input data could
nullify any performance gains
from your optimizations.
And to illustrate what I mean by
that, I'd like to share with you
a cautionary tale of two labels.
So, once upon a time, there was
a developer who needed to render
a lot of labels in her app.
And she required each label to
have a line of bold text,
followed by a line of normal
weight text.
And she needed to position her
labels by manually setting their
frames, because business
reasons.
So, she set up her labels with
attributed strings and off she
went.
But she noticed that the
scrolling performance in her app
was a little bit slower than she
would like.
So, she did a little bit of
profiling and she found that one
areas where a lot of time was
being spent was in laying out
and rendering the labels.
So, then she did some
experimentation and she noticed
that if she rendered each line
in a separate label, her app
scrolling performance improved.
''Well, this is fantastic'', she
thought.
So, she changed her app to use a
separate label for each line of
text.
And she lived happily ever
after, until her company wanted
to expand into the Chinese
market.
When she tested her app with
this Chinese text, she was
dismayed to discover that the
scrolling performance was even
slower than before.
So, what happened, here?
Well, our tragic heroine started
off with the right approach.
She was looking at frequent
rendering of lots of short
strings, which falls here, in
quadrant two.
And so, she took some
measurements, she determined an
area for improvement, and she
made an optimization.
But then, when the input data
changed to Chinese text, the
optimization was no longer an
improvement.
And to see why, let's do a
postmortem.
So, in this example, the initial
conditions qualified the
attributed strings for a faster
rendering path within the
framework.
And the optimization of
splitting each line into its own
label took advantage of the fact
that attributed strings
containing only one style of
text may qualify for faster
rendering.
But this isn't a sufficient
condition for faster rendering.
The faster rendering paths take
shortcuts that make certain
assumptions about the input data
and the rendering conditions.
And in this case, using Chinese
text requires font fallback, and
that forced the rendering down
the slower path within the
framework to maintain
correctness.
And on top of that, splitting
the two-line strings into
separate labels, meant that the
app was rendering twice as many
labels as it needed to.
And additionally, the app was
using older layout practices by
manually setting the frames,
instead of using auto layout.
So generally, we're going to pay
a lot of attention to
performance under conditions
that are using modern practices,
like auto layout.
Because that's what most apps
are using and that's where our
performance improvements will
make the largest impact.
So, with auto layout in
particular, that text system
caches some layout information.
And this can really improve
performance.
But since this app wasn't using
auto layout, it couldn't take
advantage of that.
And so, with that in mind,
here's some strategies and tips
that you can employ to improve
text layout and rendering
performance in your app.
Now, if you've been paying
attention, you already know what
I'm going to say the first
strategy is.
Use the standard labels for
rendering your text and let us
do the heavy lifting for you.
The framework is in a better
place to apply these
optimizations, because it has a
bigger picture view of the
situation and more information
about the rendering conditions.
And when we make performance
improvements, you'll
automatically get those
benefits.
So, as an example of that, in
macOS 10.13, NSTextField renders
text roughly three times faster,
at 5.7 milliseconds per frame
during live resize.
Down from 16.67 milliseconds per
frame in 10.12.
And you'll get this performance
boost for free if you're using
the standard framework controls.
So, it's really a good idea to
use the standard controls
whenever possible.
Second strategy, as we kind of
saw from out story, is to use
modern layout practices like
auto layout.
Now, text layout and rendering
performance with modern
practices is very heavily
scrutinized on our end.
And by adopting these modern
practices, you'll be less likely
to run into edge case
performance scenarios that we
haven't already seen and
improved.
Next up, is a lower level tip.
If you're working with
NSAttributedString, there are a
few attributes that are
absolutely necessary for layout
and rendering.
And if you don't supply these
attributes yourself, the text
system needs to resolve them in
order to be able to render.
And so, you can shave off a
little bit of time by supplying
these attributes yourself, when
rendering attributed strings.
In a similar vein, you might see
some small improvements from
explicitly specifying the
writing direction and alignment,
instead of using the natural
settings.
And this will save you a little
time, because the text system
can skip over any logic that
tries to figure out the writing
direction and the alignment.
But remember that you'll only
want to do this if you're
absolutely sure that your input
data won't contain mixed writing
directions.
Now, there's a balance between
performance and correctness.
And this is one optimization
that can tip the balance a
little too far away from
correctness if you're not sure
of your input.
And along those same lines of
performance versus correctness,
if you know that all your labels
are only going to consist of one
line, you can set the line break
mode to use clipping.
Now, by default, labels will use
word wrap.
And when you do this, the text
system needs to figure out where
to place the line breaks.
And so, if you use the clipping
line break mode, you can skip
this line breaking and
hyphenation logic, and your text
might run there just a little
bit faster.
So, in summary, we've looked at
a lot of different things,
today.
From performance improvements in
Foundation, to string bridging
and working with text.
Now, if you take just one thing
away from this talk, let it be
this graph.
Use the concepts of scale and
frequency to minimize the large
expense of operations in your
code.
Don't sweat the small infrequent
stuff, and always measure if you
aren't sure.
So, for more information, you
can visit our session URL, we
are Session 244.
And check out these related
sessions on video.
Pretty cool.
Unfortunately, most of them
already happened.
Thank you, and enjoy the rest of
the conference.
[ Applause ]