๐ 2020.5.14 (THU)
WWDC2016 | Session : 416 | Category : Performance
๐ Understanding Swift Performance - WWDC 2016 - Videos - Apple Developer
Swift has a variety of first class types and various mechanisms for code reuse and dynamism.
Are value or reference semantics more appropriate?
How dynamic do you need this abstraction to be?
Taking performance inplications into account often helps guide me to a more idiomatic solution.
If we want to write fast Swift code, we're going to need to avoid paying for dynamism and runtime that we're not taking advantage of.
Swift automatically allocates and deallocates memory on your behalf.
When we call into a function, we can allocate that memory that we need just by trivially decrementing the stack pointer to make space. And when we've finished executing our function, we can trivially deallocate that memory just by incrementing the stack pointer back up to where it was before we called this function.
ํจ์ ํธ์ถ ์์, ์คํ ํฌ์ธํฐ๊ฐ ๊ณต๊ฐ์ ๋ง๋ค๋๋ก ํ๋ฉด ๊ฐ๋จํ๊ฒ ๋ฉ๋ชจ๋ฆฌ ํ ๋น์ ํ ์ ์๋ค.
ํจ์ ์คํ์ด ๋๋ฌ์ ๋๋, ์คํ ํฌ์ธํฐ๋ฅผ ํจ์ ์คํ ์ ์ ์๋ ๊ณณ์ผ๋ก ๋ค์ ์ฆ๊ฐ์ํค๋ฉด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํด์ ํ ์ ์๋ค.
It's literally the cost of assigning an integer. So this is in contrast to heap, which is more dynamic, but less efficient than the stack.
integer ํ ๋นํ๋ ๊ฒ๋งํผ์ ๋น์ฉ์ด ๋ ๋ค. (๋น ๋ฅด๋ค) ๋ ๋์ ์ด์ง๋ง ํจ์จ์ ์์ข์ ํ๊ณผ ๋น๊ต ๋์กฐ๋๋ ์ ์ด๋ค.
The heap lets you do things the stack can't like allocate memory with a dynamic lifetime.
Because multiple thread can be allocating memory on the heap at the same time, the heap needs to protect its integrity using locking or other synchronization mechanisms. This is a pretty large cost.
Struct
Before we even begin executing any code, we've allocated space on the stack for our point1 instance and our point2 instance. Because point is a struct, the x and y properties are stored in line on the stack.
์ฝ๋๋ฅผ ์คํํ๊ธฐ ์ ๋ถํฐ ์ด๋ฏธ point1, point2 ์ธ์คํด์ค๊ฐ ์๋ ์คํ์ด ํ ๋น ๋๋ค. Point
๊ฐ struct ์ด๊ธฐ ๋๋ฌธ์, x
์ y
๋ ์คํ์ ์ ์ฅ ๋์ด ์๋ค.
When we go to construct our point with an x
of 0 and a y
of 0, all we're doing is initializing that memory we've already allocated on the stack.
point x, y ์ 0, 0์ ๊ตฌ์ฑํ ๋ ์ด๋ฏธ ์คํ์ ํ ๋น๋์ด ์๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ด๊ธฐํ ํด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
When we assign point1
to point2
, we're just making a copy of that point and initializing the point2
memory, again, that we'd already allocated on the stack.
point1
์ point2
์ ํ ๋นํ ๋, ๊ทธ point๋ฅผ ๋ณต์ฌํ๊ณ point2
๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ด๊ธฐํ ํ๋ฉด ๋๋ค.
Note that point1
and point2
are independent instances.
point1
๊ณผ point2
๋ ๋
๋ฆฝ๋ ์ธ์คํด์ค์ด๋ค. point2.x
์ 5๋ฅผ ํ ๋นํด๋ point1.x
์ ๊ฐ์ ์ฌ์ ํ 0์ด๋ค.
This is known as value semantics.
use point1
, use point2
and we're done executing our function. So we can trivially deallocate that memory for point1
and point2
just by incrementing that stack pointer back up to where we were when we entered our function.
Class
Instead of for the actual storage of the properties on point, we're going to allocate memory for references to point1
and point2
. References to memory we're going to be allocated on the heap.
heap ์ ํ ๋นํ ๋, Swift ๋ ์ค์ ๋ก 4word์ ์ ์ฅ ๊ณต๊ฐ์ ํ ๋น ํด์ค๋ค. ์ด ์ ์ด 2word๋ฅผ ํ ๋น ํ๋ struct ์๋ ๋์กฐ์ ์ด๋ค.
point ๊ฐ class ์ด๊ธฐ ๋๋ฌธ์, point ์ content๋ฅผ ๋ณต์ฌ ํ์ง ์๊ณ reference๋ฅผ ๋ณต์ฌํ๋ค.
This is known as reference semantics and can lead to unintended sharing of state.
We saw that classes are more expensive to construct than structs because classes require a heap allocation. Because classes are allocated on the heap and have reference semantics, classes have some powerful characteristics like identity and indirect storage.
์ด ๊ฒฝ์ฐ๊ฐ ์๋๋ผ๋ฉด Struct๋ฅผ ์ฌ์ฉํ๋๊ฒ ์ข๋ค.
And stucts aren't prone to the unintended sharing of state like classes are.
์คํ ๋ ๋, ์ ์ ๊ฐ ์คํฌ๋กค ํ ๋ ๋ง๋ค ์์ฃผ ํธ์ถ ๋๊ธฐ ๋๋ฌธ์ ๋นจ๋ผ์ผ ํ๋ค.
โ ์บ์ฑ ๋ ์ด์ด ์ถ๊ฐ
(ํ ๋ฒ ์์ฑ ๋ ์ด๋ฏธ์ง๋ ์ ์ฅ ํด๋๊ณ ๋์ด์ ์์ฑ ํ์ง ์๋๋ก)
**String
์ ํค ๊ฐ์ผ๋ก strong type ์ด ์๋๋ค.**
- ์ด๋ฏธ์ง๋ฅผ ๋ํ ํ๋ key์ด๊ธด ํ์ง๋ง ๊ทธ๋ฅ ๋๋์ด ๋ผ๊ณ ์ฝ๊ฒ ํค๋ฅผ ๋ฃ์ ์๋ ์๋ค. โ Safety ํ์ง ์๋ค.
- character๋ค์ contents ๋ฅผ ๊ฐ์ ์ ์ผ๋ก heap ์ ์ ์ฅ ํ๋ค. โ
makeBalloon
ํจ์๋ฅผ ํธ์ถ ํ ๋๋ง๋ค, ์บ์ฑ์ ํ๋๋ผ๋ heap allocation ์ ํ๊ฒ ๋๋ค.
**Struct
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ safeํ๋ค.**
Struct
๋ Swift์์ first class type์ด๊ธฐ ๋๋ฌธ์ dictionary์ key๋ก ์ฌ์ฉ ํ ์ ์๋ค.- ๋์ด์ heap allocation ์ ์๊ตฌํ์ง ์๊ณ stack์ ํ ๋นํ๋ค.
- ๋ ์์ ํ๊ณ ๋ ๋น ๋ฅด๋ค!
Swift ๋ heap์ ํ ๋น ๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ธ์ ํด์ ํด์ผ ๋๋์ง ์ด๋ป๊ฒ ์๊น?
โ Swift๋ reference์ ์ ์ฒด count ์๋ฅผ heap์ ๊ฐ์ง๊ณ ์๋ค. count ๊ฐ 0 ์ด ๋๋ฉด ํด๋น ์ธ์คํด์ค๋ฅผ ์๋ฌด๋ ๊ฐ๋ฅดํค๊ณ ์์ง ์๋ค๊ณ ํ๋จํ๊ณ , ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํด์ ํ๊ธฐ ์์ ํ๋ค๊ณ ํ๋จํ๋ค.
- multiple thread์์ count ์ฆ๊ฐ, ๊ฐ์๊ฐ ์ผ์ด๋๊ธฐ ๋๋ฌธ์ thread safety ํด์ผ ํ๋ค.
- Reference counting ์ ๊ต์ฅํ ์์ฃผ ์ผ์ด๋๋ ์ฐ์ฐ์ด๋ค. โ cost can add up.
Swift ๊ฐ retain
, release
๋ฅผ ์ถ๊ฐํด์ค๋ค.
retain
: atomically increment reference countrelease
: decrement reference count
Class
Struct
Struct ์ผ ๋๋ reference counting ์ด ์ผ์ด๋์ง ์๋๋ค.
Struct containing references
String
๋ character๋ค์ heap์ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ reference counting์ด ๋๋ค.
UIFont
๋ class ์ด๊ธฐ ๋๋ฌธ์ reference counting์ด ๋๋ค.
label
์ 2๊ฐ์ reference๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
copy๋ฅผ ํ๋ฉด reference๊ฐ 2๊ฐ ๋ ์ถ๊ฐ ๋๋ค.
Class๋ heap์ ํ ๋น ๋๊ธฐ ๋๋ฌธ์ Swift๋ heap allocation์ lifetime์ reference counting์ ํตํด ๊ด๋ฆฌ ํ๋ค.
Struct๊ฐ reference๋ฅผ ํฌํจ ํ๊ณ ์๋ค๋ฉด, reference counting ์ ํ๋ค. โ reference๊ฐ ๋ง์ ์๋ก reference counting overhead ๊ฐ ์๊ธด๋ค
๐value type UUID
More type safety, got more performance, way more convenient to write, way more type safe
๋ฉ์๋๋ฅผ runtime์ ํธ์ถํ๋ฉด, Swift correct implementation์ ์คํํด์ผ ํ๋ค.
์ปดํ์ผ ํ์์ ์คํํ implementation์ ๊ฒฐ์ ํ ์ ์๋ ๊ฒ์ด static dispatch
์ด๋ค.
๋ฐํ์์ correct implentation์ ์ง์ jump ํ ์ ์์ ๊ฒ์ด๋ค.
inlining
complier knows which implementations are going to be executed
drawAPoint(point)
โฌ๏ธ
param.draw() โ // static dispatch
โฌ๏ธ
Point.draw implementation
// no call stack overhead
Dynamic dispath ๋ ์ปดํ์ผ ํ์์ ๊ฒฐ์ ํ ์ ์๋ค. ๋ฐํ์์ implementation์ ์ฐพ๊ณ jump ํ ์ ์๋ค.
๊ทธ๋์ dynamic dispatch ๋ static๋ณด๋ค ๋น์ฉ์ด ๋น์ธ์ง ์๋ค. There's just one level of indirection.
We can create an array of these things and they're all the same size because we're storing them by reference in the array
Because this d.draw()
, it could be a point, it could be a line.
The complier adds another filed to classes which is a pointer to the type information of that class and it's stored in static memory.
And so when we go and call draw, what the compiler actually generates on our behalf is a lookup through the type to something called the virtual method table on the type and static memory, which contains a pointer to the correct implementation to execute.
Final Class
If you never intend for a class to be subclassed, you can mark it as final
struct Line
๊ณผ Point
๋ V-table dispatch ๋ฅผ ์ํด ํ์ํ ์์ ๊ด๊ณ๋ฅผ ๊ณต์ ํ๊ณ ์์ง ์๋ค.
How does Swift dispatch to the correct method?
โ Table based mechanism called the Protocol Witness Table (PWT)
ํ์ ๋ง๋ค ํ๋กํ ์ฝ์ ๊ตฌํํ ํ๋์ ํ ์ด๋ธ์ด ์๋ค. ํ ์ด๋ธ์ ์ํธ๋ฆฌ๊ฐ ํ์ ์ ๊ตฌํ์ ๋งํฌํ๋ค.
Line
์ 4 words ๊ฐ, Point
๋ 2 words ๊ฐ ํ์ โ ๊ฐ์ ์ฌ์ด์ฆ๊ฐ ์๋๋ฐ array๋ fixed offset
**Point
(2 words)**
The first 3 words in that existential container are reserved for the valueBuffer.
Small types like our Point
, which only needs two words, fit into this valueBuffer.
**Line
(4 words)**
Swift allocates memory on the heap and stores the value there and stores a pointer to that memory in existential container
- Allocate memory on the heap and store a pointer to that memory inside of the valueBuffer of the existential container
- Swift needs to copy the value from the source of the assignment that initializes our local variable into existential container
- Copy entries of our value witness table will do the correct thing and copy it into the valueBuffer allocated in the heap
- Program continues and we are at the end of the lifetime of our local variables
- Swift calls the deallocate function in that table
โ This work is what enables combining value types such as struct Line
and struct Point
together with protocols to get dynamic behavior, dynamic polymorphism.
- Existential Container inline
- Large values on the heap
- Supports dynamic polymorphism
โ This representation allows storing a differently typed value later in the program.
heap ์ expensive ํ๋ฐ ? 4 ๊ฐ heap ์ ์ฌ์ฉ?
โ existential container has place for 3 words and references would fit into those 2 words
first๋ฅผ ๋ณต์ฌํด์ second๋ฅผ ๋ง๋ค๋ฉด reference count ๋ง ์ฆ๊ฐ ์ํค๋ฉด ๋์ง๋ง unintended sharing์ด ์ผ์ด๋์์?
โ Copy on write
When we come to modify, mutate our value, we first check the reference count. Is it greater than 1 ?
If the reference count is greater than 1, we create a copy of our Line storage and mutate that.
This is a lot cheaper than heap allocation.
- Dynamic polymorphism
- Indirection through Witness Tables and Existential Container
- Copying of large value causes heap allocation
Swift will bind the generic type T to the type used at this call side.
Generic parameter T in this call context is bound through the type Point.
Because we have one type per call context, Swift does not use an existential container here. Instead, it can pass both the value witness table of the type used at this call-site as additional arguments to the function.
And then during execution o that function, when we create a local variable for the parameter, Swift will use the value witness table to allocate potentially any necessary buffers on the heap and execute the copy from the source of the assignment to the destination.
Is this any faster? Is this any better?
This static form of polymorphism enables the compiler optimization called specialization of generics.
If we compile those 2 files separately, when I come to compile the file UsePoint
, the definition of my Point
is no loner available because the compiler has compiled those 2 files separately.
However, with whole module optimization, the compiler will compile both files together as one unit and will have insight into the definition of the Point
file and optimization can take place.
Performance characteristics like class type
- Heap allocation on creating an instance
- Reference counting
- Dynamic method dispatch through V-Table