App Frameworks • iOS, macOS • 37:32
Border crossings can be smooth and efficient or slow and tedious. The boundary between an app and a framework is analogous, but inefficiencies may not always be obvious. Whether it’s passing data to an API or understanding how to stay on the fast path while rendering text, we'll explore various aspects of how an app can work efficiently with underlying frameworks. If you build your own frameworks, gain valuable insights into how your clients can be as efficient as possible.
Speakers: Philippe Hausler, Donna Tom
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
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 ]