Explore Swift Performance
Oct 26, 2024


My name is John McCall and today we are going to explore Swift performance.
안녕 내이름은 John McCall 그리고 오늘은 Swift Performance에 대해서 알아볼 겁니다.
When you work a lot in a programming language, it's important to have a good intuition for the performance of different operations in that language.
프로그래밍 언어로 많은 작업을 수행할 때는 해당 언어의 다양한 작업 성능에 대한 좋은 직관을 갖는 것이 중요합니다.
Programmers coming from C often have that.
C 프로그래머들이 그러합니다.
For better or worse, the translation of C into machine code is pretty literal.
좋건 나쁘건, C를 기계어 코드로 번역하는 것은 문자 그대로입니다.

Local variables like these are allocated on the stack.
위와 같은 지역변수들은 스택에 저장됩니다.
Heap allocations only happen if you make a call.
힙 할당은 호출을 하는 경우에만 발생합니다.
The compiler might still move things into registers, optimize memory, and find all sorts of other clever ways to make things faster.
컴파일러는 항목을 레지스터로 이동하고, 메모리를 최적화하고 그리고 작업속도를 높이기 위해 기타 모든 종류의 똑똑한 방법을 찾을 수 있습니다.
But there's a baseline for how things are compiled that you can feel confident about.
하지만, 어떻게 컴파일이 될지에 대한 자신감이 되는 기준선이 있습니다.
Swift is not always so simple.
Swift는 항상 단순하지는 않습니다.
Partly that has to do with safety, that nice little translation you get from C will happily scribble all over memory if your code is wrong.
어떤 부분은 안전과 관련이 있습니다.
부분적으로는 안전과 관련이 있습니다. C에서 얻은 멋진 작은 번역은 코드가 잘못된 경우 메모리 전체에 낙서해서 즐겁게 할것입니다.(망친다는 얘기)
But Swift also provides a lot of tools for abstraction that C doesn't, closures, generics, and so on.
그러나 Swift는 C에서 제공하지 않는 추상화(Closure, Generic 등)를 지원합니다.
Those abstractions have non-trivial implementations with costs that aren't quite as visible as an explicit call to malloc.
이러한 추상화에는 malloc같이 비용이 있고 중요한 실행이 포함되어 있지만, 보이지 않는 실행들을 내포하고 있습니다.
But that doesn't mean that you can't develop a similar intuition for how your code will actually run.
하지만, 코드가 어떻게 실행될지에 직관적으로 개발할 수 없다는 뜻은 아닙니다.
And that's critical for when you need to do performance work.
그리고 이러한 개발은 성능 작업을 수행해야 할 때 매우 중요합니다.
So in this talk, we're going to explore the low-level performance of Swift.
그래서 이번 강연에서는 Swift의 low-level 성능에 대해서 알아봅시다.
First, we're going to talk about what we mean by performance.
첫번째로
성능이란 무엇인지
에 대해서 알아봅시다.Next, we'll talk about the principles that you should do be thinking about, when you look at low-level performance.
다음으로 low-level 성능을 볼 때 고려해야 할 원칙에 대해서 이야기해봅시다.
And we'll end by exploring some of the details of how key features in Swift are implemented, and what impact that has on performance.
그리고 마마지막으로, Swift의 핵심기능이 어떻게 구현되는지, 그리고 그것이 성능에 어떤 영향을 미치는지에 대한 세부사항을 살펴보는 것으로 마무리하겠습니다.
So what do we mean by performance? That's a pretty deep question.
그렇다면 성능이란 무엇인가요? 이건 꽤 심도깊은 질문입니다.
It'd be nice if you could just take your program and feed it to some tool and have it spit out a single number, that was all we needed to know about performance.
프로그램을 가져와서 어떤 도구에 넣고 [성능 점수 X점] 이렇게 나오면 좋을것 같습니다.
이것이 우리가 성능에 대해서 알아야할 전부였습니다.
Maybe Safari has a performance score of 9.2
예를들면 사파리는 9.2점!
Sadly, that's not how it works.
슬프게도 Performance(성능)은 이렇게 작동하지 않습니다.
Performance is multidimensional and situational.
성능은 다차원적이고 상황에 따라 달라집니다.
Usually, we care about performance because of some macroscopic problem.
Our daemon is drawing too much power, or our UI feels awkwardly slow to click around in, or our app keeps getting jetsam’ed.
보통 거시적인 문제 때문에 우리는 성능을 신경씁니다.
거시적인 문제는 이런걸 뜻하죠. 데몬이 너무 많은 전력을 소모하고 있거나, UI가 클릭하기가 이상할 정도로 느리게 느껴지거나, 앱이 계속해서 혼란스러울 때 말입니다.
When you're investigating these problems, you generally work top down.
우리가 이러한 문제를 조사할 때, 일반적으로 top-down 방식으로 조사합니다.
You make measurements with tools like instruments, and that shows you places to dig in on.
instruments같은 툴로 조사해서 무엇을 파고들어야할지 보여줍니다.
A lot of the time, you're going to solve these problems with algorithmic improvements, without ever getting into the low-level performance of your code.
대다수의 경우, 코드의 low-level 성능에 대해서 고려하지 않고도, 알고리즘 개선을 통해서 이러한 문제를 해결하게 될 겁니다.
But sometimes you do need to dig into low-level performance.
하지만 때때로 low-level 성능에 대해서 파고들어야 할 것입니다.
Maybe you've narrowed your investigation down to a single part of the execution trace, and there's not much more you can do with it at an algorithmic level.
어쩌면, 문제인 부분을 추려냈고, 알고리즘 수준에서 더 이상 할 수 있는 일이 없을 수도 있습니다.
It just seems slow.
걍 느려보입니다.
Going further requires understanding how your code is actually running, and that requires a more bottom-up approach.
더 나아가려면, 코드가 어떻게 실행되는지 이해야하며, bottom-up 방식의 접근이 필요합니다.

Low-level performance tends to be dominated by these four considerations.
low-level 성능은 다음 4가지 고려 사항에 대해서 좌우되는 경향이 있습니다.
First, we're doing a lot of calls that aren't being optimized effectively.
1. 효과적으로 최적화되지 않은 많은 호출을 수행하고 있습니다.
Second, we're wasting a lot of time, or memory, because of how our data is represented.
2. 데이터가 표현되는 방식 때문에 많은 시간 혹은 메모리를 낭비하고 있습니다.
Third, we're spending too much time allocating memory, or fourth, we're spending a lot of time unnecessarily copying and destroying values.
3. 메모리 할당에 너무 많은 시간을 소비하고 있습니다.
4. 불필요한 Value 타입의 값 복사/제거에 너무 많은 시간을 소비하고 있습니다.
Most features in Swift have implications for one or more of these costs.
Swift의 대부분 기능은 위 비용 중 하나 이상에 영향을 끼칩니다.
I'm going to get into all of them, but let me add one last consideration first.
모두 다룰긴 하지만, 먼저 마지막 고려 사항을 하나 추가하겠습니다.
Swift has a powerful optimizer.
Swift는 강력한 최적화 프로그램이 있습니다.
There are things that you never see as performance issues, because the compiler does a good job at eliminating them.
컴파일러가 퍼포먼스 이슈에 대해서 잘 제거해주기 때문에, 성능 문제로 결코 볼 수 없는 것들이 있습니다.
There are limits to the optimization.
하지만, 최적화에는 늘 한계가 있습니다.
The way you write code can have a significant impact on how much the optimizer can do.
당신이 코드를 어떻게 작업을 하냐에 따라서 컴파일러의 최적화 프로그램의 성능이 좌우 될 것입니다.
(코드를 작성하는 방식은 컴파일러의 최적화 프로그램이 수행할 수 있는 작업에 상당한 영향을 끼칠 수있습니다.)
So as I go through this, I’m also going to be talking about optimization potential, because it’s an important part of programming for performance.
그렇기에 최적화 가능성에 대해서도 이야기하겠습니다. 왜냐하면 프로그래밍 퍼포먼스의 중요한 한 부분이기 때문입니다.
If you’re uncomfortable relying on the optimizer, let me make a suggestion.
만약 컴파일러의 최적화에 의존하는 것이 불편하다면, 다음을 제안합니다.
When performance is an important part of your project, you need to be regularly monitoring it.
성능이 프로젝트에 중요한 부분인 경우에는 규칙적으로 모니터링해야합니다.
So when you identify hot spots during top-down investigation, try to find ways to measure them, and then automate those measurements as part of your development process.
따라서 하향식 조사 중에 문제점을 식별한 경우, 문제 부분을 측정하도록 측정 방법을 찾고, 프로세스의 일부로 측정을 자동화해서 모니터링할 수 있도록 하세요.
(따라서 하향식 조사 중에 문제점을 식별한 경우 이를 측정하는 방법을 찾은 다음에 개발 프로세스의 일부로 해당 측정을 자동화하세요.)
Once you’re doing that, you’re well set up to identify regressions, whether it’s because you somehow confused the optimizer or just because you accidentally added some quadratic algorithm.
그렇게 하고 나면, 최적화 프로그램을 혼동했기 때문이든, 단지 실수로 2차 알고리즘을 추가했기 때문이든 회귀를 식별할 수 있는 준비가 잘 된 것입니다.
And now you’re verifying that the optimizer is still doing what you’re asking of it.
이제 컴파일러의 최적화 기능이 요청한 작업을 계속 수행하고 있는지 확인할 수 있습니다.
With all that said, let’s dig on those four principles of low-level performance.
이상으로 low-level 성능의 4가지 원칙을 살펴보겠습니다.
The first is function calls.
첫 번째로 함수 호출이빈다.
There are four costs associated with function calls.
함수 호출에 관련있는 비용은 4가지가 있습니다.

Three of them are things that we do.
4개 중 3가지는 우리가 하는 일 입니다.
First, we have to set up the arguments for the call.
첫 번째로 함수 호출에 대한 인수를 설정해야 합니다.
We also have to resolve the address of the function we’re calling.
우리는 또한 호출하는 함수의 주소를 확인해야 합니다.
And we have to allocate space for the function’s local state.
그리고 함수의 loacl 상태를 위해서 공간을 할당해야 합니다.
The fourth is something that we don’t do: This whole thing might inhibit optimization, both in the caller and in the function it calls.
So.
마지막 4번째는 우리가 하는게 아닙니다: 이 모든 것이 호출자와 호출하는 함수 모두에서 최적화를 방해(억제)할 수 있습니다.
Four costs. Let’s look at argument passing first.
4가지 비용, 먼저 인수 전달을 살펴봅시다.
There’s two levels to this cost.
이 비용에는 2개 레벨이 있습니다.
At the lowest level, when we make a call, we have to put arguments in the right place for the calling convention.
가장 낮은 수준에서, 우리가 호출을 할 때, 호출 규칙에 맞는 올바른 위치에 인수를 넣어야 합니다.
On modern processors, these costs are usually hidden by register renaming, so they don’t make much difference in practice.
최신 프로세서에서는 이러한 비용이 일반적으로 레지스터 이름 변경으로 인해 숨겨지므로 실제로는 큰 차이가 없습니다.
At a higher level, though, the compiler may have to add copies of values to match the ownership conventions of the function.
그러나 더 높은 수준에서는 컴파일러가 함수의 소유권 규칙에 맞게 값 복사를 추가해야 할 수도 있습니다.
This often does show up in profiles as extra retains and releases, on one side of the call or the other.
이는 종종 추가적인 retain과 release로 나타납니다. 호출하는 쪽 혹은 호출당하는 쪽에
I’ll come back to this in a few minutes.
이건 조금 있다가 다시 이야기 하겠습니다.
The next two costs, function resolution and optimization impact, both come down to the same issue: Do we know at compile time exactly which function we’re calling? If so, we say the call uses static dispatch; otherwise, it uses dynamic dispatch.
다음 두가지 비용 함수 함수 해결(Dispatch라고 이해하면 될 듯하다.) 최적화 임팩트(최적화 영향 및 결과) 같은 이슈로 귀결된다.
→ 컴파일 타임에 우리가 호출하는 함수를 특정할 수 있습니까?
- 그렇다면, 우리는 static dispatch라고 부른다.
- 그렇지 않다면, 우리는 dynamic dispatch라고 부른다.
*Function Resolution (함수 해결)
Function resolution is the process by which a system determines which specific function to execute when a function is invoked.
Static dispatch is more efficient, it’s a little faster at the processor level, but more importantly, there’s a lot of optimization that’s possible at compile time, like inlining and generic specialization, if the compiler can see that function definition.
But dynamic dispatch is what enables polymorphism and other powerful tools for abstraction.
(정적 디스패치)Static Dispatch는 더 효율적이고 프로세서단에서 조금 더 빠르다.
그러나 더 중요한 것은, 컴파일러가 함수 정의를 볼 수 있다면, 컴파일 타임에 더 많은 최적화가 가능하다는 것이다. (inlining과 generic 특수화 등 같은 최적화)
But dynamic dispatch is what enables polymorphism and other powerful tools for abstraction.
그러나 dynamic dispatch(동적 디스패치)는 다형성과 추상화를 위한 강력한 도구를 가능하게 만듭니다.

In Swift, only specific kinds of calls use dynamic dispatch, and you can tell by looking at the declaration you’re calling.
Swift에서는 특정 종류의 호출만 동적 디스패치를 사용하며, 호출하는 선언을 보면 이를 알 수 있습니다.

In this example, I have a call to update on a value of protocol type.
What kind of call this is depends on where the method is declared.
이 예에서, 프로토콜 유형의 값을 업데이트하는 호출이 있습니다. 이 호출의 종류는 메서드가 어디서 호출했냐에 따라 다릅니다.
If it’s declared in the main body of the protocol, it’s a protocol requirement, and the call to it uses dynamic dispatch.
프로토콜 본문에 선언된 경우 프로토콜 요구 사항이며, 이에 대한 호출은 동적 디스패치를 사용합니다.
But if it’s declared in a protocol extension, the call uses static dispatch.
그러나
프로토콜 + extension
으로 선언했을 경우 static Dispatch를 사용합니다.This is a really important difference, both semantically and for performance.
이건 의미론적으로도, 성능상으로도 중요한 차이입니다.

The last cost of function calls is allocating memory for local state.
In order to run, this function needs some memory.
함수 호출의 마지막 비용은 로컬 상태에 대한 메모리 할당입니다. 이 함수를 실행하려면 약간의 메모리가 필요합니다.
It’s an ordinary, synchronous function, so it allocates that memory on the C stack.
이건 일반적인 동기 함수이므로 C 스택에 메모리를 할당합니다.
Allocating space on the C stack can be done by just subtracting from the stack pointer.
C 스택에 공간을 할당하려면 스택 포인터에서 빼기만 하면 됩니다.
(왜냐하면 스택은 (높은 주소 → 낮은 주소)로 쌓이므로)
If you compile this, you’ll get assembly code that manipulates the stack pointer at the start and end of the function.
이것을 컴파일하면 함수의 시작과 끝에 있는 스택 포인터를 조작하는 어셈블리 코드를 얻게 됩니다.

When we enter the function, the stack pointer is pointing into the C stack.
함수로 진입하면, stack pointer는 C 스택을 가리키고 있습니다.
We start by subtracting some number from the stack pointer in our assembly, we can see it’s 208 bytes.
우리는 어셈블리의 스택 포인터에서 어떤 숫자를 빼는 것으로 시작합니다. 그러면 208바이트라는 것을 알 수 있습니다.
This allocates what’s traditionally called the CallFrame, and it gives the function space to execute.
이는 전통적으로 CallFrame이라고 불립니다. CallFrame은 함수를 할당하고 실행할 수 있는 공간을 제공합니다.
Now we can run the body of the function.
이제 우리는 함수 본문을 실행시킬 수 있습니다.
Right before we return, we add 208 bytes back to the stack pointer, deallocating the memory we allocated before.
함수가 return되게 전에 이전에 더했던 208bytes 만큼 stack pointer를 뒤로 돌려서 이전에 할당했던 메모리를 해제시킵니다.

You can think of the CallFrame as having a layout like a C struct.
CallFrame은 C의 구조체와 유사한 레이아웃을 가지고 있다고 생각할 수 있습니다.
Ideally, all of the local state of the function just becomes fields in the CallFrame.
이상적으로는, local 함수는 CallFrame의 필드가 되는 것입니다.
Now the reason I say putting things in the CallFrame is ideal is that the compiler is always going to emit that subtraction at the start of the function.
CallFrame에 뭔가를 넣는 게 이상적이라고 말하는 이유는 컴파일러가 항상 함수 시작 부분에서 뺄셈을 내보낼 것이기 때문입니다.
- 함수 시작시에는 (고메모리 → 저메모리) 이기에 함수 포인터를 빼야합니다.
It has to, in order to make space to save critical things like the return address.
return 값과 같은 중요한 정보를 저장할 공간을 확보하기 위해서는 그래야 합니다.
Subtracting a larger constant doesn’t take any longer, so if we need memory in the function, allocating it as part of the CallFrame is as close as it gets to free.
큰 상수를 빼는 데는 더 오랜 시간이 걸리지 않으므로, 함수에서 메모리가 필요한 경우 CallFrame의 일부로 할당하는 것이 거의 무료와 같습니다.
That ties nicely into the next low-level principle we want to look at, which is memory allocation.
이는 우리가 다음으로 살펴보고자 하는 low-level 원칙, 메모리 할당과 잘 연결됩니다.
Traditionally, we think of there as being three kinds of memory.
전통적으로 우리는 3가지 메모리를 떠올립니다. Global / Stack / Heap
Of course, to the computer, these all ultimately come from the same pool of RAM; but we allocate and use them in different patterns, on our programs.
물론 컴퓨터입장에서는 결국 이 모든 것들은 같은 Ram pool에서 옵니다. 그러나 우리의 프로그램에서는 다르 패턴으로 할당하고 사용합니다.
And that’s significant to the operating system, which makes it significant for performance.
이는 성능에도 중요하므로 운영체제 입장에서도 중요합니다.

Global memory is allocated and initialized when the program is loaded.
This isn’t free, but it’s close to it.
전역 변수(Global memory)는 프로그램 로딩 때 할당되고 초기화된다.
완전 무료는 아니지만, 무료에 가깝습니다.
The big drawback with global memory is that it only works for specific patterns with a fixed amount of memory that will live for the entire duration of the program.
전역 변수의 큰 단점은 특정 패턴(코드)에만 사용됨에도 불구하고 프로그램이 돌아가는 동안 메모리가 유지된다는 것입니다.
That matches well with global variables and static member variables, but not with much else.
글로벌 변수와 static 멤버 변수 등이 이에 해당합니다.

We already talked about CallFrames as an example of stack allocation.
우리는 이미 스택 할당의 예로 CallFrames에 대해 이야기했습니다.
Like global memory, stack memory is very cheap, but it only works in certain patterns.
글로벌 메모리와 마찬가지로 스택 메모리는 매우 저렴하지만 특정 패턴(코드)에서만 작동합니다.
In this case, the memory has to be scoped: there has to be a point in the current function where we’re guaranteed that there will be no more uses of that memory.
이 경우 메모리는 범위가 지정되어야 합니다. 즉, 현재 함수에서 더 이상 해당 메모리를 사용하지 않을 것이라는 보장이 있는 지점이 있어야 합니다.
That matches well with a typical local variable.
이러한 개념은 일반적인 지역변수와 잘 일치합니다.
The last kind of memory is the heap.
마지막 메모리 종류인 heap에 대해 알아봅시다.
Heap memory is very flexible: you can allocate it at an arbitrary time and deallocate it in an arbitrary time later.
힙 메모리는 매우 유연합니다. 임의의 시간에 이를 할당하고 나중에 임의의 시간에 해제할 수 있습니다.
That flexibility makes allocation and deallocation substantially more expensive than either of the other kinds.
임의의 시간에 할당해고 해제할 수 있는 유연함이 다른 종류 메모리보다 비쌉니다.
The heap is used for some obvious things like class instances, but it’s also used for some features where we simply don’t have strong enough static lifetime restrictions to use anything else.
힙은 클래스 인스턴스와 같은 몇 가지 명백한 것에 사용되지만, 생명주기를 확정지을 수 없을 때 사용되기도 합니다. (단순히 static 생명주기를 제한 할 수 있을 만큼 강한 제한을 갖지 못한 일부 기능에도 사용됩니다.)
Often, when we allocate heap memory, the memory ends up having shared ownership, which means we have multiple, independent references to the same memory.
종종 힙 메모리를 할당하면 메모리는 공유 소유권을 가지게 됩니다. 즉, 동일한 메모리에 대한 여러 개의 독립적인 참조가 있게 됩니다.
Swift manages the lifetime of those allocations with reference counting.
Swift는 이를 ARC를 통해서 생명주기를 관리합니다.
In Swift, we call incrementing the reference count a retain, and decrementing the reference count a release.
Swift에서는 참조 카운트를 증가시키는 것을 retain이라고 하고, 참조 카운트를 감소시키는 것을 release라고 합니다.
Now that we’ve talked about allocating memory, I want to cover how Swift uses that memory to store values.
이제 메모리 할당에 대해 이야기했으니, Swift가 이 메모리를 사용하여 값을 저장하는 방법에 대해 알아보고 싶습니다.
We call this memory layout.
우리는 이것을 Memory Layout이라고 부릅니다.
In most conversations about Swift, when we talk about values, we’re usually talking about a high-level concept, irrespective of what’s stored where in memory.
Swift에 대한 대부분의 대화에서 값에 대해 이야기할 때 우리는 일반적으로 메모리의 어디에 저장되어 있는지와 상관없이 high-level의 컨셉에 대해 이야기합니다.

For example, after this initialization, we might say that the value of this variable is an array of two doubles.
예를 들어, 위 사진처럼 초기화 이후에 우리는 두개의 double로 구성된 배열이라고 말할 수 있습니다.
When we need to talk about how things look in memory, sometimes people still use the word value.
우리가 메모리에 어떻게 보이는지에 대해서 이야기 할 때, 때대로 사람들은 여전히
value
라는 단어를 사용합니다.Now in this talk, that’s going to be confusing, so I’ll use the more technical word representation instead.
이 세션에서는 그렇게하면 혼란스러울 테니, 조금 더 기술적인 단어를 대신해서 사용하겠습니다.
The representation of a value is how it’s arranged in memory.
값의 표현은 메모리에 어떻게 배열되어있는지를 표현합니다.

The variable array is a name for memory that holds a reference to a buffer object that’s currently initialized with the representations of two double values.
변수
array
는 현재 두 개의 double 값의 표현으로 초기화된 버퍼 객체에 대한 참조를 보관하는 메모리의 이름입니다.I'll also use the phrase inline representation to mean just the portion of the representation that you get without following any pointers.
또한, 어떤 포인터도 따르지 않고 얻는 표현의 일부만을 의미하기 위해 인라인 표현이라는 문구를 사용하겠습니다.
So the inline representation of our variable array is a single buffer reference, ignoring what that buffer actually contains.
따라서 변수 배열의 인라인 표현은 단일 버퍼 참조이며, 해당 버퍼가 실제로 담고 있는 내용은 무시됩니다.
The MemoryLayout type in the standard library just measures inline representation.
표준 라이브러리의 MemoryLayout 유형은 인라인 표현만을 측정합니다.
- inline이라는 표현을 표면상이라고 읽어도 좋다.
So for array, it’s just 8 bytes, the size of a single 64-bit pointer.
따라서 array는 단지 8포인트입니다.64bit 프로그램의 1개 포인터 사이즈죠.
Okay.
좋습니다.

Every value in Swift is part of some containing context.
Swift의 모든 값은 [어떤 포함 컨텍스트]의 일부입니다.
Local scopes contain all of the values used within them: local variables, intermediate results, and so on.
로컬 범위에는 범위 내에서 사용되는 모든 값(로컬 변수, 중간 결과 등)이 포함됩니다.
Structs and classes contain all of their stored properties.
구조체와 클래스에는 모든 저장 속성이 포함되어 있습니다.
Arrays and Dictionaries contain all of their elements via their buffer and so on.
배열과 Dictionary는 모든 elements의 buffer 등을 포함하고 있습니다.
Every value in Swift also has a type.
Swift의 모든 값은 type을 갖고 있습니다.
The value’s type dictates how the value is represented in memory, including its inline representation.
값의 유형은 인라인 표현을 포함하여 값이 메모리에 표현되는 방식을 결정합니다.
The value’s context dictates where the memory comes from to hold the inline representation.
값의 컨텍스트는 인라인 표현을 보관하기 위한 메모리가 어디에서 나오는지 지시합니다.
So let’s see how that looks in our example.
그럼 어떻게 보이는지 예를 통해서 봅시다.

Our array is a local variable.
우리의 array는 지역 변수 입니다.
So we have an array value, that’s contained by a local scope.
그래서 array 값은 local scope 안에 포함됩니다.
Local scopes place inline representations in the function’s CallFrame if they can.
로컬 범위는 가능한 경우 함수의 CallFrame에 인라인 표현을 배치합니다.
That's gonna work here, so somewhere in this CallFrame, there’s space for the inline representation of an Array of Double.
이건 여기서 가능합니다. 그래서 CallFrame 어딘가에 Array of Double를 위한 공간이 존재합니다.
What is the inline representation of an Array of Double?
Array of Double의 inline 표현은 무엇입니까?

Well, Array is a struct, and the inline representation of a struct is just the inline representation of all its stored properties.
Array은 구조체이고, 구조체의 inline 표현은 저장된 프로퍼티의 inline 표현일 뿐입니다.
If you look at the standard library source code, this could be a little hard to see, but I’ll give you a spoiler: At the end of the day, Array has a single stored property, and it’s a class reference.
표준 라이브러리 코드를 보시면, 보기에는 어렵겠지만, 스포일러를 드리겠습니다. 결국 Array에는 단일 저장 속성이 있으며 이는 클래스 참조입니다.
And a class reference is just a pointer to an object.
그리고 클래스 참조는 객체에 대한 포인터일 뿐입니다.
So really, our CallFrame just stores that pointer.
실제로 우리의 CallFrame은 해당 포인터를 저장합니다.
In Swift, structs, tuples, and enums, all use inline storage
Swift에서는 구조체, 튜플 그리고 열거형 모두 inline storage를 사용합니다.
Everything they contain is laid out inline in their container, typically in the order it was declared.
컨테이너(CallFrame인가?)에 포함된 모든 내용은 일반적으로 선언된 순서대로 컨테이너 내에 배치됩니다.

Classes and actors use out-of-line storage: Everything they contain is laid out inline in an object, and the container just stores a pointer to that object.
클래스와 액터는 out-of-line 저장소를 사용합니다.: 포한된 모든 내용은 객체에 inline으로 배치되고 컨테이너는 객체에 대한 포인터만 저장됩니다.
This difference has major performance implications.
이러한 차이는 성능에 큰 영향을 미칩니다.
To explain those, I need to talk about our last low-level principle, which is value copying.
설명하자면, 우리의 low-level 마지막 원칙인 value copying에 대해서 설명해야합니다.
Now, there’s a basic concept in Swift called ownership.
이제 Swift에는 소유권이라는 기본 개념이 있습니다.
Ownership of a value means responsibility for managing that value’s representation.
값에 대한 소유권은 값의 표현을 관리할 책임을 의미합니다.

We just saw that the inline representation of an Array is a reference to a buffer object.
우리는 배열의 inline 표현이 버퍼 객체에 대한 참조라는 것을 방금 보았습니다.
References like this are managed using reference counting.
이와 같은 참조는 참조 카운팅을 사용하여 관리됩니다.
When we say that a container has ownership of an Array value, that means there’s an invariant that the underlying array buffer has been retained as part of storing the value into the container.
컨테이너가 배열 값의 소유권을 가지고 있다고 말할 때, 이는 컨테이너에 값을 저장하는 과정의 일부로 기본 배열 버퍼가 유지된다는 불변성이 있음을 의미합니다.
The container is then responsible for eventually balancing that retain with a release.
그런 다음 컨테이너는 결국 해당 retain과 release 균형을 맞추는 역할을 합니다.

If nothing else, that has to happen when the container goes away in this example, where the container is a local scope, the object will be released when the variable goes out of scope.
다른 것이 없더라도 이 예에서는 컨테이너가 사라질 때 이 작업이 수행되어야 합니다. 컨테이너가 로컬 범위인 경우 변수가 범위를 벗어나면(함수 밖으로 나가면) 객체가 해제됩니다.
Any use of a value or variable in Swift interacts with this ownership system somehow; this is a key part of memory safety.
Swift에서 값이나 변수를 사용하면 이 소유권 시스템과 어떤 식으로든 상호 작용합니다. 이는 메모리 안전성의 핵심적인 부분입니다.

There are three main kinds of ownership interaction: a value can be consumed, it can be mutated, or it can be borrowed.
3가지 중요한 소유권과 상호작용하는게 있습니다.
- 값을 소비하거나
- 변경하거나
- 빌릴 수 있습니다.

Consuming a value means transferring ownership of its representation from one place to another.
값을 소비한다는 것은 그 표현의 소유권을 한 곳에서 다른 곳으로 이전한다는 것을 의미합니다.
The most important operation that naturally needs to consume a value is assigning the value into memory.
값을 소모하는 가장 중요한 연산은 값을 메모리에 할당하는 것입니다.

Now that happens in our example: Initializing a variable, requires us to transfer ownership of the initial value into the variable.
이제 예제에서 그런 일이 발생합니다.: 변수를 초기화하려면 초기값의 소유권을 변수로 이전해야 합니다.
Sometimes we can do that without any copies.
때로는 복사본 없이도 이를 수행할 수 있습니다.
In this example, the initial value of the variable is an array literal, which naturally produces a new, independent value.
이 예에서 변수의 초기 값은 배열 문자그대로 이며 자연스럽게 새로운 새로운 독립 값을 생성합니다.
Swift can just transfer the ownership of that value directly into the variable.
Swift는 소유권을 변수에 직접 전송할 수 있습니다.

If we initialize a second variable with the value of the first, we again need to transfer ownership of a value into the new variable.
첫 번째 변수의 값으로 두 번째 변수를 초기화하는 경우, 다시 값의 소유권을 새 변수로 이전해야 합니다.
But now the initial value expression does not naturally produce a new value: It just refers to an existing variable.
그러나 이제 초기 값 표현식은 자연스럽게 새 값을 생성하지 않고, 기존 변수를 참조할 뿐입니다.
We can’t just steal the value out of that variable, because there might be more uses of it.
우리는 그 변수의 값을 그냥 훔칠 수 없습니다. 그 변수의 용도가 더 많을 수도 있거든요.
In order to get an independent value, we have to copy the current value of the old variable.
독립적인 값을 얻기 위해서는, 우리는 기존 변수(
array
)로부터 새로운 값(array2
)으로 복사해야 합니다.Since the value is an array, copying it means retaining its buffer.
값이 배열이기 때문에. 복사는 buffer를 유지한다는 의미입니다.
Now, this is something that’s frequently optimized.
이것은 자주 최적화 되는 것입니다.
If the compiler can see that aren’t any more uses of the original variable, it should be able to transfer the value here without a copy.
컴파일러가 기존 변수의 더 이상 용도가 없다는 것을 알면 복사 없이 여기에 값을 전송할 수 있어야 합니다.
You can also use the consume operator to request this explicitly.
또한, consumer 연산자를 사용하여 이를 명시적으로 요청할 수 있습니다.

If you do try to use the variable past this point where it's explicitly consumed Swift will complain and tell you there isn’t a value there anymore.
이 지점을 지나서 명시적으로 소비된 변수를 사용하려고 하면 Swift는 불평하고 더 이상 값이 없다고 말할 것입니다.

The second way to use a value is mutation.
두번째로 값을 사용하는 방법은 mutation(변경) 입니다.
Mutating a value means temporarily taking ownership of the current value stored in some mutable variable.
값을 변경한다는 것은 변경 가능한 변수에 저장된 현재 값의 소유권을 일시적으로 취득한다는 것을 의미합니다.
The key difference from consuming is that the variable still expects to have ownership of the value afterwards.
consuming(소비)와 주된 차이점은 변수가 이후에도 값의 소유권을 가질 것으로 기대한다는 것입니다.

When you call a mutating method like this, you’re transferring ownership of the value currently in the variable over to the method.
이런 식으로 mutating 메서드를 호출하면 현재 변수에 있는 값의 소유권이 메서드로 이전됩니다.
Swift will prevent you from simultaneously using the variable in any other way during the call.
Swift에서는 호출 중에 변수를 다른 방식으로 동시에 사용하는 것을 방지합니다.
When the method is done, it transfers ownership of the new value back to the variable.
이 메서드가 완료되면 새 값에 있던 소유권이 다시 변수로 이전됩니다.
This maintains the invariant that the variable has ownership of its value.
이렇게 하면 변수가 해당 값에 대한 소유권을 갖는다는 불변성이 유지됩니다.

The last way to use values is to borrow them.
값을 사용하는 마지막 방법은 brrow(빌리는 것)입니다.
Borrowing a value means asserting that nobody else can consume or mutate it.
값을 빌린다는 것은 다른 누구도 그 가치를 소비하거나 변형할 수 없다고 주장하는 것을 의미합니다.
This is what you naturally want to do when you just want to read a value.
이는 단순히 값을 읽고 싶을 때 자연스럽게 하려는 작업입니다.
All you care about is that nobody else is changing or destroying that value out from under you.
그 어떤 누구도 당신 허락 없이 값을 바꾸거나 파괴할 수 없다는 것입니다.
Passing an argument is one of the most common situations that usually should just borrow.
인수를 전달하는 것은 가장 흔하게 값을 borrow(빌려)하여 사용하는 방법 중 하나입니다.

If I pass my array to print, ideally that should just pass the information along without doing any extra work.
배열을 print에 전달하면 이상적으로는 추가 작업 없이 정보만 전달되어야 합니다.
However, there are some situations where Swift needs to defensively copy arguments instead of borrowing them.
그러나 Swift가 인수를 빌리는 대신 방어적으로 복사해야 하는 경우도 있습니다.
In order to borrow a value, Swift has to prove that there aren’t any simultaneous attempts to mutate or consume it.
값을 빌리려면 Swift는 동시에 값을 변형하거나 소비하려는 시도가 없음을 증명해야 합니다.
In this simple example, it should be able to do that reliably.
이 간단한 예에서는 이를 안정적으로 수행할 수 있어야 합니다.

In more complex examples, it sometimes struggles.
더 복잡한 예에서는 때때로 어려움을 겪습니다.
When the storage is in a class property, for example, it can be hard for Swift to prove that the property isn’t modified at the same time, so it may need to add a defensive copy.
예를 들어 저장소가 클래스 속성에 있는 경우, Swift에서 속성이 동시에 수정되지 않았다는 것을 증명하기 어려울 수 있으므로 방어적 복사본을 추가해야 할 수 있습니다.
This is an area where Swift is actively evolving improvements, both with improvements to the optimizer, and with new features to let you explicitly borrow values to avoid copies.
Swift는 최적화 도구를 개선하고, 복사를 피하기 위해 값을 명시적으로 빌릴 수 있는 새로운 기능을 추가함으로써 이 분야를 적극적으로 개선하고 있습니다.

What does it actually mean to copy a value? It depends on the inline representation of the value.
값을 복사한다는 것은 실제로 무엇을 의미합니까? 값의 인라인 표현에 따라 달라집니다.
Copying a value means copying the inline representation so that you get a new inline representation with independent ownership.
값을 복사한다는 것은 인라인 표현을 복사하여 독립적인 소유권을 가진 새로운 인라인 표현을 얻는다는 의미입니다.
That means that copying a class value means copying the ownership of the reference, which just means retaining the object it refers to.
즉, 클래스 값을 복사한다는 것은 참조의 소유권을 복사하는 것을 의미하는데, 이는 참조하는 객체를 그대로 유지한다는 것을 의미합니다.
Copying a struct value means recursively copying all of the struct’s stored properties.
구조체 값을 복사한다는 것은 구조체의 저장된 모든 속성을 재귀적으로 복사한다는 것을 의미합니다.

That means that choosing between inline and out-of-line storage involves some real trade-offs.
즉, 인라인 스토리지(value type)와 아웃오브라인 스토리지(reference value) 중 하나를 선택하는 데는 실제로 어느 정도의 타협이 필요하다는 뜻입니다.
Inline storage avoids allocating memory on the heap and for small types, this is great.
인라인 스토리지는 힙에서 메모리를 할당하지 않으며 작은 유형의 경우 더 좋습니다.
For larger types, the cost of copying can become a significant drag on performance, if you find yourself doing a lot of copies.
더 큰 유형의 인쇄물을 복사하는 경우, 많은 복사 작업을 하게 되면 복사 비용이 성능에 상당한 부담이 될 수 있습니다.
There’s no hard-and-fast rule here for getting the best performance.
최상의 성과를 얻기 위한 확고한 규칙은 없습니다.
The cost of copying large structs comes in two parts.
큰 구조체를 복사하는 데 드는 비용은 두 가지 부분으로 나뉩니다.

First, when we’re copying value types, we’re often not just copying bits.
첫째, 값 유형을 복사할 때 단순히 비트만 복사하는 것이 아닌 경우가 많습니다.
These three stored properties are all represented using object references that will have to be retained when we copy the enclosing structs.
이 세 개의 저장된 속성은 모두 객체 참조를 사용하여 표현되며, 이 참조는 둘러싼 구조체를 복사할 때 유지되어야 합니다.
So, if we made this a class, copying it would have to do a retain of the class object.
따라서 이것을 클래스로 만들면 복사할 때 클래스 객체를 유지해야 합니다.
But copying it as a struct actually still does three retains of these individual fields.
하지만 이를 구조체로 복사하면 실제로 개별로 각자 소유하고 있습니다.
(하지만 이를 구조체로 복사하면 실제로 개별 필드를 세 번 유지합니다.)
Also, each copy of this value will need its own storage for all these stored properties.
또한, 이 값의 각 사본에는 저장된 모든 속성에 대한 별도의 저장소가 필요합니다.
So if we expect to copy this value around a lot, we may end up using a lot more memory.
따라서 이 값을 많이 복사하려고 하면 결국 훨씬 더 많은 메모리를 사용하게 될 수 있습니다.
If this type used out-of-line storage instead, each copy would refer to the same object, so the memory would be re-used.
대신 이 유형이 heap 메모리를 사용하면, 각 복사본이 동일한 객체를 참조하므로 메모리가 재사용됩니다.
Again, no hard-and-fast rules, but something you should be thinking about.
다시 말해, 엄격한 규칙은 없지만, 생각해봐야 할 사항입니다.

Now in Swift we encourage you to write types with value semantics, where a copy of the value behaves like it’s totally unrelated to where you copied it from.
이제 Swift에서는 value 타입을 사용하여 유형을 작성하도록 권장합니다. 즉, 값의 복사본이 복사한 위치와 전혀 관련이 없는 것처럼 동작합니다.
Structs behave this way, but always use inline storage.
Struct는 항상 inline으로 저장됩니다.
Class types use out-of-line storage, but they naturally have reference semantics.
Class는 outline 저장소를 사용합니다, 하지만 기본적으로 참조를 갖고 있습니다.

One way to get both out-of-line storage and value semantics is to wrap the class in a struct and then use copy-on-write.
outline 저장소를 사용하면서 값 의미를 사용하는 방법은 Class를 Struct로 감싸고 COW를 사용하는 것이다.
The standard library uses this technique in all of Swift’s fundamental data structures, like Array, Dictionary, and String.
표준 라이브러리는 Array, Dictionary, String과 같은 Swift의 모든 기본 데이터 구조에서 이 기술을 사용합니다.
+) 프로토콜도 다음과 같이 사용함.
We’ve spent a lot of time talking about these four basic principles.
As part of that, we’ve seen how they translate into some basic Swift features like structs, classes, and functions.
우리는 이 네 가지 기본 원칙에 대해 많은 시간을 들여 이야기했습니다.
그 일환으로, 우리는 그것들이 구조체, 클래스, 함수와 같은 일부 기본 Swift 기능으로 어떻게 변환되는지 살펴보았습니다.
Now let’s put them together to talk about some high-level features in Swift.
이제 이를 종합하여 Swift의 몇 가지 고급 기능에 대해 알아보겠습니다.
We’ll start with dynamically-sized types.
먼저 동적으로 크기가 조정되는 유형부터 시작하겠습니다.
Structs in C are always constant-size, but Swift types can have a size determined at runtime.
C의 Struct는 항상 일정한 크기이지만, Swift에서는 런타임에 크기가 결정될 수 있습니다.
That comes up in two cases.
크기가 런타임에 결정되는건 2가지 경우가 있습니다.
First, many value types in the SDK reserve the right to add and change their stored properties in a future OS update, that includes types like Foundation’s URL.
SDK의 많은 값 유형은 향후 OS 업데이트에서 저장된 속성을 추가하고 변경할 수 있는 권한을 보유합니다. 마치 Foundation의 URL 처럼요.
This means that everything about their layout has to be treated as unknown at compile time.
즉, 컴파일 시점에는 레이아웃에 대한 모든 내용을 알 수 없는 것으로 처리해야 합니다.
Second, a type parameter of a generic type can usually be replaced by any type with any possible representation, so again we have to treat its layout as unknown.
두 번째로, generic 매개변수는 일반적으로 모든 가능한 표현을 가진 모든 형식으로 대체될 수 있으므로 레이아웃을 알 수 없는 것으로 처리해야 합니다.
Now there’s an exception to this second rule when the type parameter is constrained to be a class.
단, 두 번째 경우에 예외가 있습니다. generic 파라미터가 class인 경우입니다.
In this case, we know that it has to have the representation of a class type, which is to say, always a pointer.
위와 같은 경우, class 타입의 generic 이라는건 다시 말하자면 항상 포인터라는 의미입니다.
This can lead to much more efficient code even when generic substitution doesn’t kick in, if you’re able to accept the constraint.
이건 더 효율적인 코드를 만들 수 있습니다.
만약 제네릭 타입을 클래스로 바꿀 수 있다면 훨씬 더 효율적인 코드를 만들 수 있습니다. 제네릭 대체가 제대로 작동하지 않더라도요.

Alright. How does Swift handle memory layout and allocation when the compiler doesn’t statically know the representation of a type?
좋습니다. 컴파일러가 정적으로 표현 타입들을 모른다면 Swift는 메모리 layout을 어떻게 처리하고 할당할까요?
컴파일러가 유형의 표현을 정적으로 알지 못할 때 Swift는 메모리 레이아웃과 할당을 어떻게 처리합니까?
Well, it depends on what kind of container is storing the value.
글쎄요, 어떤 종류의 Container가 값을 저장하냐에 따라 달려있습니다.
For most containers, Swift can just do the layout at runtime.
대부분 containers에 대해서 Swift는 런타임에 layout을 할 수 있습니다.

For example, this Connection struct contains a URL.
예를 들어 이
Connection
Struct는 URL을 갖고 있습니다.Because the layout of URL isn’t known statically, the layout of Connection can’t be known statically, either.
URL의 정적인 layout을 모르기에,
Connection
Struct의 layout또한 정적으로 알 수 없다.But that’s fine, it just becomes the problem of whatever contains the Connection.
그러나 괜찮습니다, 그건 단지
Connnection
Struct가 contain한 요소의 문제이니깐요.The compiler knows the static layout of Connection up until it reaches the first dynamically-sized property.
컴파일러는 첫번째 dynamically-sized 변수에 도달하기 전까지는
Connection
layout을 알고 있습니다.The rest of the layout will be filled in dynamically by the Swift runtime, the first time this program needs the layout of the type.
나머지 layout은 Swift 런타임에 채워질 것이며, 프로그램이 해당 유형의 layout을 필요할 때 할당할 것입니다.

If URL ends up being 24 bytes, then Connection will be laid out at runtime exactly like the compiler would’ve laid it out if it had known that statically.
URL이 24바이트가 되면 Connection은 컴파일러가 정적으로 알고 있는 것과 똑같은 방식으로 런타임에 배치됩니다.
The compiler will just have to load sizes and offsets dynamically instead of being able to use constants.
컴파일러는 상수처럼 사용하는 대신, 크기와 오프셋을 동적으로 load해야 합니다.
Some containers, however, must have constant size.
그러나, 일부 컨테이너는 반드시 일정한 크기를 가져야합니다.
In these cases, the compiler must allocate memory for the value separately from the main allocation for the container.
이러한 경우, 컴파일러는 컨테이너의 기본 할당과 별도로 값에 대한 메모리를 할당해야 합니다.
For example, the compiler can only request constant amounts of global memory.
예를 들어, 컴파일러는 일정한 양의 전역 메모리만 요청할 수 있습니다.
If you make a global variable of a type like URL, the compiler will create a global variable of pointer type.
URL 유형과 같은 전역변수를 만들면, 컴파일러는 포인터 유형의 전역 변수를 생성합니다.
When you access the global variable for the first time, as part of lazily running its initializer, Swift will also lazily allocate space for it on the heap.
처음으로 전역 변수에 접근할 때, 전역 변수의 lazy 특성으로 인해서, Swift는 힙 공간에 천천히 할당하게 됩니다.
→ 처음 접근 할 때 메모리에 할당된다는 이야기.

A similar thing happens with local variables, because CallFrames must also have a constant size.
하지만 이와 같은 일이 로컬 변수에도 비슷하게 발생합니다. 왜냐하면 CallFrames도 일정한 크기를 가져야 하기 때문입니다.
The CallFrame just contains a pointer to the URL.
CallFrame은 URL에 대한 포인터만 갖고 있습니다.
When the variable comes into scope, the function will have to allocate it dynamically, then free it when it goes out of scope.
지역 변수가 범위에 들어오면, 함수는 동적으로 할당해야하고 범위로 나가면 해제하게 됩니다.
However, because local variables are scoped, this allocation can still be done on the C stack.
하지만, 지역 변수는 범위가 지정되므로, 이 할당은 C stack에서 수행될 수 있습니다.
When we enter the function, we allocate the CallFrame as normal.
함수에 들어가게 되면, 우리는 CallFrame을 정상적으로 할당합니다.

When the variable comes into scope, we simply do another subtraction from the stack pointer for the size of the variable.
변수들이 범위 내로 들어오면, 우리는 단순히 스택 포인터에서 변수의 크기만큼 뺄셈을 합니다.
When the variable goes out of scope, we can reset the stack pointer to what it was before.
변수가 범위 밖으로 나가면, 우리는 stack pointer를 이전 상태로 되돌릴 수 있습니다.

So far, we’ve only been talking about synchronous functions.
지금까지, 우리는 sync 함수(동기화 함수)에 대해서 이야기했습니다.
What about async functions? The central idea with async functions is that C threads are a precious resource, and holding on to a C thread just to block is not making good use of that resource.
비동기 함수의 핵심 아이디어는 C 스레드가 귀중한 리소스이고, 차단하기 위해 C 스레드를 유지하는 것은 그 리소스를 제대로 활용하지 못한다는 것입니다.
As a result, async functions are implemented in two special ways:
결과적으로 async 함수는 2가지 특별한 방법으로 구현됩니다.
First, they keep their local state on a separate stack from the C stack, and second, they’re actually split into multiple functions at runtime.
첫째, C stack과 별도의 스택에 로컬 상태로 유지하고,
둘째, 런타임시 실제로 여러 함수로 분리되는 것입니다.
So, let’s look at an example async function.
예시를 한번 봅시다.

There’s one potential suspension point, await, in this function.
이 함수에는 잠재적인 await 가 있습니다.
All of these local functions have uses that cross that suspension point, so they can’t be saved on the C stack.
이러한 모든 로컬 함수는 해당 중단 지점을 넘는 용도가 있으므로 C 스택에 저장할 수 없습니다.
We just talked about how sync functions allocate their local memory on the C stack by subtracting from the stack pointer.
우리는 방금 sync 함수가 스택 포인터에서 빼는 방식으로 C 스택에 로컬 메모리를 할당하는 방법에 대해 이야기했습니다.
Async functions conceptually work the same way, except they don’t allocate out of a large, contiguous stack.
async 함수는 개념적으로 같은 방식으로 작동합니다. 크고 연속적인 stack(main stack)에 할당하지 않는 점을 제외하고요.
Instead, async tasks hold on to one or more slabs of memory.
대신에 async 작업은 하나 이상의 메모리를 보유합니다.
When an async function wants to allocate memory on the async stack, it asks the task for memory.
async 함수가 async stack에 메모리 할당을 원하면, 작업에 메모리를 요청합니다.

The stack tries to satisfy that from the current slab and if it can, great.
스택은 현재 slab에서 이를 만족시키려고 시도하며, 가능하다면 좋습니다.
*slab
- 리눅스 커널 메모리 관리 시스템에서 중요한 역할을 하는 메모리 할당 메커니즘
- 커널 객체들의 할당과 해제를 최적화 함
- 메모리 할당/해제의 오버헤드 감수
- 메모리 단편화 최소화
- 캐시 효율성 향상
The task will mark that part of the slab as used and give it to the function.
이 Task는 slab의 해당 부분을 사용된 것으로 표시하고 함수에 전달합니다.

If most of the slab is occupied however, that allocation might not fit.
그러나 slab의 대부분이 점유되어 있다면 해당 할당이 맞지 않을 수 있습니다.(메모리 단편화)
The task then has to allocate a new slab with malloc.
그러면 다음 작업은 malloc으로 새 slab를 할당해야 합니다.
And the allocation comes out of that.
그리고 그로부터 할당이 나옵니다.
In either case, deallocation just hands the memory back to the task, where it becomes marked as unused.
어느 경우든, 할당 해제는 단지 메모리를 task에 돌려주고, task에서는 사용되지 않는 것으로 표시합니다.
Because this allocator is only used by a single task and uses a stack discipline, it's typically significantly faster than malloc.
왜냐하면 이 할당자는 단일 task에만 사용되며 stack 규칙을 사용하므로, 일반적으로 malloc보다 훨씬 빠릅니다.
The overall performance profile is similar to that of synchronous functions, just with a bit higher overhead for calls.
전체적인 성능 프로필은 sync 함수와 비슷합니다. 단지 호출에 있어 약간의 오버헤드가 있을 뿐입니다.

Now in order to actually run, an async function must be split into partial functions that span the gaps between the potential suspension points of the function.
이제 실제로 실행하려면 비동기 함수를 함수의 잠재적인 중단 지점 사이의 간격을 메우는 부분 함수로 분할해야 합니다.
In this case, because there’s one await in the function, we end up with two partial functions.
이 경우 함수에 await가 하나 있으므로 부분 함수가 두 개 생깁니다.
The first partial function starts with the entry to the original function.
첫 번째 부분 함수는 원래 함수의 항목으로 시작합니다.
If the array is empty, it will just return to the async caller.
배열이 비어 있으면 비동기 호출자로 반환됩니다.
Otherwise, it pulls the first task out and awaits it.
그렇지 않으면 첫 번째 작업을 꺼내서 기다립니다.
The other partial function picks up after that await.
그 후에 다른 부분 함수가 대기합니다.
First, it adds the result of the task it awaited to the output array, then tries to continue the loop.
먼저, 기다리던 작업의 결과를 results 배열에 추가한 후, 루프를 계속 진행하려고 합니다.
If there are no more tasks, it returns to the async caller.
더 이상 작업이 없으면 비동기 호출자로 돌아갑니다.
Otherwise, it will loop back and await the next task.
그렇지 않으면 루프로 돌아가 다음 작업을 기다립니다.
The key idea here is that there’s only at most one partial function on the C stack.
여기서 핵심 아이디어는 C 스택에 부분 함수가 최대 하나만 있다는 것입니다.
We enter one partial function and run like an ordinary C function until the next potential suspension point.
우리는 하나의 부분 함수에 들어가서 다음 잠재적인 중단 지점까지 일반 C 함수처럼 실행합니다.
If the partial function needs some local state that doesn’t have to cross a suspension point, it can allocate that into its C CallFrame.
부분 함수에 중단 지점을 넘을 필요가 없는 로컬 상태가 필요한 경우 해당 상태를 C CallFrame에 할당할 수 있습니다.
At that point, the partial function tail-calls the next partial function.
그 시점에서 부분 함수는 다음 부분 함수를 호출합니다.
Its CallFrame disappears from the C stack, and the frame for the next is allocated.
CallFrame이 C 스택에서 사라지고, 다음에 대한 프레임이 할당됩니다.
Then that partial function runs until it reaches a potential suspension point.
그런 다음 해당 부분적 기능은 잠재적인 중단 지점(await)에 도달할 때까지 실행됩니다.
If a task ever needs to actually suspend, it just returns normally on the C stack, which will typically go directly to the concurrency runtime so the thread can immediately be re-used for something else.
작업을 실제로 일시 중단해야 하는 경우에는 일반적으로 C 스택에서 반환되고, 이는 일반적으로 동시성 런타임으로 직접 전송되어 스레드를 다른 작업에 즉시 재사용할 수 있습니다.
In my examples so far, I’ve always shown a func declaration.
여태까지 함수에 대해서 알아봤습니다.
How do closures work, and what impact do they have on local allocation?
그럼 클로저는 어떻게 작동하며 지역 할당에 어떤 영향을 끼칠까요?
Closures are always passed around as values of function type.
클로저는 항상 함수 유형의 값으로 전달됩니다.

This function takes an argument that’s a non-escaping function.
이 함수는 이스케이프되지 않는(non @escaping) 함수인 인수를 사용합니다.

Function values in Swift are always a pair of a function pointer and a context pointer.
Swift의 함수 값은 항상 함수 포인터와 컨텍스트 포인터의 쌍입니다.
So in C terms, this function signature looks something like this.
따라서 C 용어로 표현하면, 이 함수 서명은 다음과 같습니다.

A call to the function value in Swift simply calls the function pointer, passing the context pointer as an implicit extra argument.
Swift에서 함수 값을 호출하는 것은 단순히 함수 포인터를 호출하고, 컨텍스트 포인터를 암묵적인 추가 인수로 전달합니다.

A closure expression that captures values from the enclosing scope has to package those values up into the context.
클로저 표현에서 값을 캡처하는 클로저 표현식은 해당 값을 컨텍스트에 패키징해야 합니다.
How this works depends on the kind of function value it has to produce.
이것이 어떻게 작동 방식은 생성해야 하는 함수 값의 종류에 따라 달라집니다.

In this case, the function is a non-escaping function.
이 경우 함수는 non-escaping 함수입니다.
As a result, we know that the function value will not be used after the call completes, which means it does not need to be memory-managed and we can allocate the context with a scoped allocation.
결과적으로, 호출이 완료된 후에는 함수 내 값이 사용되지 않을 것이므로 메모리를 관리할 필요가 없고 범위 할당을 통해 컨텍스트를 할당할 수 있습니다.

The context will therefore just be a simple structure containing the captured value.
그러므로 컨텍스트는 캡처된 값을 포함하는 간단한 구조가 됩니다.
The context can be allocated on the stack, and the address of that will be passed to sumTwice.
컨텍스트는 스택에 할당될 수 있으며, 해당 주소는
sumTwice
에 전달됩니다.In the closure function, we know the type of the paired context and can just pull the data we need out of it.
클로저 함수에서 우리는 페어링된 컨텍스트의 유형을 알고 있으며, 필요한 데이터를 끌어올 수 있습니다.

This is different for escaping closures.
@escaping closure와는 다릅니다.
We no longer know, that the closure will only be used within the duration of the call.
우리는 클로저가 함수 호출 기간 동안만 사용 될 것이라는 것을 더 이상 알 수 없습니다.
Therefore, the context object must be heap-allocated and managed with retains and releases.
따라서 컨텍스트 객체는 힙에 할당되어야 하며 retain과 release를 통해 관리되어야 합니다.
The context essentially behaves like an instance of an anonymous Swift class.
컨텍스트는 본질적으로 익명의 Swift 클래스의 인스턴스처럼 동작합니다.
Now in Swift, when you refer to a local var in a closure, you capture that variable by reference.
이제 Swift에서는 클로저에서 로컬 변수를 참조할 때 참조로 해당 변수를 캡처합니다.
This allows you to make changes to the variable that will be observed in the original scope and vice-versa.
이렇게 하면 원래 범위에서 관찰되는 변수를 변경할 수 있으며, 그 반대의 경우도 마찬가지입니다.

If the var is only captured by non-escaping closures, this doesn’t change the lifetime of the variable.
var가 non-escaping 클로저에 의해서만 캡처되는 경우, 이는 변수의 수명을 변경하지 않습니다.
As a result, the closures can handle this by just capturing a pointer to the variable’s allocation.
결과적으로 클로저는 변수 할당에 대한 포인터를 캡처하는 것만으로 이를 처리할 수 있습니다.

But if the var is captured by an escaping closure, the lifetime of the var can be extended for as long as the closure is alive.
하지만 var가 escaping 클로저에 의해 캡처되는 경우 var의 수명은 클로저가 살아 있는 한 연장될 수 있습니다.
As a result, the var also has to be heap-allocated, and the closure context has to retain a reference to that object.
결과적으로 var도 힙에 할당되어야 하고, 클로저 컨텍스트는 해당 객체에 대한 참조를 유지해야 합니다.
Let’s wrap up by talking about generics.
제네릭에 대해 이야기하면서 마무리해 보겠습니다.

This function is generic over its data model.
이 함수는 data model에 대해서 generic입니다.
We already talked about how the layout of this type is statically unknown and how that’s handled in different containers.
우리는 이미 이 유형의 레이아웃이 statically unknown 방식과 그것이 다양한 컨테이너에서 어떻게 처리되는지에 대해 이야기했습니다.
We haven’t yet talked about how protocol constraints work.
우리는 아직 프로토콜 제약이 어떻게 작동하는지에 대해 이야기하지 않았습니다.
In particular, how does Swift actually execute this call that uses a protocol requirement?
특히, Swift는 프로토콜 제약상황에서 실제로 어떻게 작동할까요?
Swift protocols are represented at runtime with a table of function pointers, one for each requirement in the protocol.
Swift 프로토콜은 프로토콜의 각 요구 사항에 대해 하나씩 함수 포인터 표로 런타임에 표현됩니다.
That table looks roughly like this in C.
C에서는 해당 표가 대략 다음과 같습니다.
Any time we have a protocol constraint, we’re passing around a pointer to the appropriate table.
프로토콜 제약이 있을 때마다 우리는 적절한 테이블에 대한 포인터를 전달합니다.

In a generic function like this, the type and witness tables become hidden extra parameters.
이와 같은 generic 함수에서는 type과 witness 테이블이 숨겨진 추가 매개변수가 됩니다.
Everything in this signature at runtime corresponds straightforwardly to something from the original Swift signature.
런타임 시 이 서명의 모든 내용은 원래 Swift 서명의 내용과 직접적으로 대응합니다.
When we work with values of protocol type, it’s different.
프로토콜 유형의 값을 사용하는 경우에는 다릅니다.

In this case, we’ve made this function more flexible each element of the array is now allowed to be a different type of data model.
이 경우, 배열의 각 요소가 다른 유형의 데이터 모델이 될 수 있도록 함수를 더 유연하게 만들었습니다.

But that has trade-offs for how efficiently it will run.
하지만 그렇게 하려면 얼마나 효율적으로 운영해야 할지에 대한 균형이 필요합니다.
The inline representation of a protocol like AnyDataModel looks like this in C.
AnyDataModel과 같은 프로토콜의 인라인 표현은 C에서 다음과 같습니다.
We have storage for the value and fields to record the value’s type and any conformances we know it has.
우리는 값을 저장할 수 있는 저장소와 값의 유형과 우리가 알고 있는 모든 적합성을 기록할 수 있는 필드를 갖고 있습니다.
But this has to be a fixed-size type its representation can’t change sizes in order to support different types of data model.
하지만 이것은 고정 크기 유형이어야 하며 다양한 유형의 데이터 모델을 지원하기 위해 표현 크기를 변경할 수 없습니다.
No matter how large we make the value storage, there’s potentially going to be a data model that won’t fit into it.
아무리 value 저장소를 크게 만들어도 그에 맞지 않는 데이터 모델이 생길 가능성이 있습니다.

What do we do? Swift uses an arbitrary buffer size of 3 pointers.
우리는 무엇을 하나요? Swift는 3개의 포인터로 이루어진 임의의 버퍼 크기를 사용합니다.
If the value stored in a protocol type can fit into that buffer, Swift will put it there, inline.
프로토콜 유형에 저장된 값이 해당 버퍼에 들어갈 수 있는 경우, Swift는 그 값을 인라인으로 넣습니다.
Otherwise, it allocates space for the value on the heap and just stores that pointer in the buffer.
버퍼에 들어갈 수 없는 경우 힙에 값을 저장할 공간을 할당하고 해당 포인터를 버퍼에 저장합니다.

So these function signatures look very similar, but they actually have very different characteristics.
따라서 이러한 함수 signatures은 매우 유사해 보이지만 실제로는 매우 다른 특성을 가지고 있습니다.
The first function takes a homogeneous array of data models.
첫 번째 함수는 동질적인 데이터 모델 배열을 사용합니다.
Those data models will be efficiently packed in the array.
해당 데이터 모델은 배열에 효율적으로 압축됩니다.
The type information will be passed once to the function, as separate top-level arguments.
type 정보는 별도의 최상위 인수로 함수에 한 번 전달됩니다.
The function can also be specialized if the caller knows what type it’s being called with.
호출자가 호출되는 유형을 알고 있는 경우 함수를 특수화할 수도 있습니다.

Here we’re calling it on an array with a known type.
여기서는 알려진 유형의 배열에서 호출합니다.
The optimizer can easily either inline this call or produce a specialized version of the function that works with this exact argument type.
optimizer는 이 함수 호출을 쉽게 인라인으로 처리하거나 해당 인수 유형으로 작동하는 함수의 특수 버전을 생성할 수 있습니다.
This removes any abstraction cost associated with generics, making the update call go directly to its implementation in MyDataModel’s conformance.
이렇게 하면 제네릭과 관련된 모든 추상화 비용이 제거되어 업데이트 호출이 MyDataModel의 적합성에 따라 직접 구현으로 이동합니다.

The second function takes a heterogeneous array of data models.
두 번째 함수는 다양한 종류의 데이터 모델을 사용합니다.
This is more flexible, if you’ve got data models of different types, it’s probably what you need.
이 방법은 더 유연합니다. 여러 유형의 데이터 모델이 있는 경우 아마 이 방법이 필요할 겁니다.
But each element of the array now has its own dynamic type, and the values won’t be densely packed in the array.
하지만 배열의 각 요소는 이제 자체 동적 유형을 가지며 값은 배열에 압축되어 있지 않습니다.

Optimizing this in practice is also much more difficult; the compiler would have to perfectly reason about how data flows into the array and gets used in the function.
이를 실제로 최적화하는 것은 훨씬 더 어렵습니다. 컴파일러는 데이터가 배열로 어떻게 흘러들어오고 함수에서 어떻게 사용되는지 완벽하게 추론해야 합니다.
Now, that doesn’t completely doom your performance, but it does mean you’ll be getting a lot less help from the compiler in this one place.
이제, 이것이 여러분의 성능을 완전히 망치는 것은 아니지만, 컴파일러로부터 받는 도움이 훨씬 줄어든다는 것을 의미합니다.
And as I wrap up this talk, that’s what I want to leave you with.
그리고 이 강연을 마치면서, 제가 여러분께 남기고 싶은 말씀은 다음과 같습니다.
Please don’t come away from this thinking: "John told us to not use protocol types.
"존이 프로토콜 유형을 사용하지 말라고 했어."라고 말했다고 그 말만 맹신하지 마세요.
" Everything I mentioned as a cost in this talk is just that: It's a cost, and sometimes costs are worth paying.
"제가 이 강연에서 비용으로 언급한 모든 것은 단지 비용일 뿐이며, 때로는 비용을 지불할 가치가 있습니다.
Because abstraction is a powerful and useful tool, and you should take advantage of it.
추상화는 강력하고 유용한 도구이므로, 이를 활용하는 것이 좋습니다.
I hope the information in this talk helps you to develop an intuition for the performance of your Swift code.
이 강연의 정보가 Swift 코드의 성능에 대한 직관을 키우는 데 도움이 되기를 바랍니다.
Thank you for watching!
시청해 주셔서 감사합니다!
Share article