1. ์บ์๋?
CPU๋ ๋งค์ฐ ๋น ๋ฅด์ง๋ง, RAM์ ์๋์ ์ผ๋ก ๋๋ฆผ
๊ทธ๋์ CPU์ RAM ์ฌ์ด์ ์์ ๊ณ ์ ๋ฉ๋ชจ๋ฆฌ์ธ โ์บ์(Cache)"๊ฐ ์กด์ฌํจ
- L1,L2,L3 ์บ์ : ์ ์ ๋๋ฆฌ์ง๋ง ํฌ๊ธฐ๋ ์ปค์ง
- CPU๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋:
- ์บ์์ ์์ผ๋ฉด -> Cache Hit (๋น ๋ฆ)
- ์์ผ๋ฉด -> Cache Miss (๋๋ฆผ, RAM๊น์ง ๋ด๋ ค๊ฐ)
2. Cache Hit vs Cache Miss
- Cache Hit : CPU๊ฐ ์ฐพ๋ ๋ฐ์ดํฐ๊ฐ ์บ์์ ์ด๋ฏธ ์กด์ฌ : ๋น ๋ฆ (์ ns)
- Cache Miss : CPU๊ฐ ์ฐพ๋ ๋ฐ์ดํฐ๊ฐ ์บ์์ ์์ -> RAM์์ ๊ฐ์ ธ์ด (์์ญ~์๋ฐฑ ns)
3. Spatial & Temporal Locality (์ง์ญ์ฑ)
CPU ์บ์๋ ์ง์ญ์ฑ(Locality)์ ๊ธฐ๋ฐ์ผ๋ก ์๋ํจ
- Temporal Locality (์๊ฐ์ ์ง์ญ์ฑ):
- ์ต๊ทผ ์ฌ์ฉํ ๋ฐ์ดํฐ๋ ๋ ์ฌ์ฉํ ๊ฐ๋ฅ์ฑ ๋์
- Spatial Locality (๊ณต๊ฐ์ ์ง์ญ์ฑ):
- ์ธ์ ํ ๋ฐ์ดํฐ๋ ๊ฐ์ด ์ฌ์ฉํ ๊ฐ๋ฅ์ฑ ๋์
๊ทธ๋์ CPU๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ๋, ๊ทผ์ฒ ๋ฉ๋ชจ๋ฆฌ ๋ฉ์ด๋ฆฌ(=Cache Line) ์ ์ฒด๋ฅผ ๋ฏธ๋ฆฌ ๋ถ๋ฌ์ด (๋ณดํต ํ cache line์ 64๋ฐ์ดํธ)
4. ์บ์ ๋ฏธ์ค๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ
- ๋ถ์ฐ์ ๋ฉ๋ชจ๋ฆฌ ์ ๊ทผ
- ๋ฌด์์ ํฌ์ธํฐ ์ ๊ทผ
- ํฐ ๊ตฌ์กฐ์ฒด๋ฅผ ์ง๋์น๊ฒ ๋ณต์ฌ
- ๋ค์ค ์ฐ๋ ๋์์ ๋์ผํ ์บ์๋ผ์ธ ์ ๊ทผ (false sharing)
5. Unity/ECS ๊ด์ ์์
ECS + Chunk ๊ตฌ์กฐ๋ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋์์ธ๋จ
- Chunk๋ ๋ฐ์ดํฐ๊ฐ ์ฐ์๋ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ๋จ
- ๋ฐ๋ผ์ for-loop๋ก ์ํํ ๋ Cache Hit๊ฐ ๊ทน๋ํ
- ๋ฐ๋๋ก, GameOjbect + MonoBehaviour๋ ๊ฐ ๋ฐ์ดํฐ๊ฐ ์ฐ๋ฐ์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ์ ์์ -> Cache Miss ์์ฃผ ๋ฐ์
6. ์์ ์ฝ๋ ๋น๊ต (Cache Friendly vs Cache Unfriendly)
[Cache Unfriendly]
class Unit {
public float posX, posY, velX, velY;
}
Unit[] units = new Unit[100000];
for (int i = 0; i < units.Length; i++) {
units[i].posX += units[i].velX;
}
-> ๊ฐ Unit์ด ๋ฉ๋ชจ๋ฆฌ ์ ํฉ์ด์ ธ ์์ด ์บ์ ๋ฏธ์ค ๋ฐ์
[Cache Friendly - ECS ์คํ์ผ]
struct Position { public float x, y; }
struct Velocity { public float x, y; }
NativeArray<Position> positions;
NativeArray<Velocity> velocities;
for (int i = 0; i < positions.Length; i++) {
positions[i].x += velocities[i].x;
}
-> ์ฐ์๋ ๋ฉ๋ชจ๋ฆฌ, ์บ์ ํํธ์จ ๋์
- CPU ์บ์๋ RAM๋ณด๋ค ๋น ๋ฅธ ์ค๊ฐ ์ ์ฅ์
- Cache Hit = ๋น ๋ฅด๊ฒ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- Cache Miss = RAM ์ ๊ทผ -> ๋๋ ค์ง
- ์ฐ์๋ ๋ฉ๋ชจ๋ฆฌ ์ ๊ทผ & ์ง์ญ์ฑ ๊ณ ๋ ค๊ฐ ํต์ฌ
- Unity ECS๋ ์บ์ ์ต์ ํ๋ฅผ ์ํด ์ค๊ณ๋จ
๋ฉํฐ์ค๋ ๋ฉ ์ฑ๋ฅ ์ต์ ํ : False Sharing๊ณผ Data Alignment ์ดํดํ๊ธฐ
1. False Sharing (๊ฐ์ง ๊ณต์ )
๋ฉํฐ์ค๋ ๋ฉ ํ๊ฒฝ์์ ๋ ๊ฐ ์ด์์ ์ค๋ ๋๊ฐ ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์ ๊ทผํ๋ค๊ณ ํด๋, ๊ทธ ๋ฐ์ดํฐ๋ค์ด ๊ฐ์ ์บ์ ๋ผ์ธ(Cache Line)์ ์กด์ฌํ๋ฉด ๋ฌธ์ ๊ฐ ์๊ธธ ์ ์์
์ ๋ฌธ์ ๊ฐ ๋๋๊ฐ?
- CPU๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ ๋ผ์ธ ๋จ์(64๋ฐ์ดํธ ๋ฑ)๋ก ๋ถ๋ฌ์ด
- ์ค๋ ๋ A๊ฐ ๋ณ์ a๋ฅผ ์์ -> ๊ฐ์ ์บ์ ๋ผ์ธ์ ์๋ ์ค๋ ๋ B์ ๋ณ์ b๋ ๋ถํ์ํ๊ฒ ๋ฌดํจํ๋จ
- ๊ทธ ๊ฒฐ๊ณผ, CPU๋ ์บ์๋ฅผ ๊ณ์ ๋๊ธฐํ(sync)ํด์ผ ํ๋ฉฐ, ์ฑ๋ฅ ์ ํ๋ก ์ด์ด์ง
์์
public class SharedData
{
public int a; // Thread 1 ์ฌ์ฉ
public int b; // Thread 2 ์ฌ์ฉ
}
- a์ b๋ ์๋ก ๋ค๋ฅธ ์ฐ๋ ๋๊ฐ ์ฌ์ฉํ์ง๋ง, ๊ฐ์ 64๋ฐ์ดํธ ์ ๋)์ ๋ค์ด๊ฐ ์์ ํ๋ฅ ์ด ๋์
- Thread 1์ด a๋ฅผ ์์ -> CPU๋ a๊ฐ ์๋ ์บ์ ๋ผ์ธ์ ๋ค๋ฅธ ์ฝ์ด์ ๋๊ธฐํํด์ผ ํจ
- ๊ทธ ๊ฒฐ๊ณผ Thread 2๋ b์ ์ ๊ทผํ ๋ ๋ถํ์ํ๊ฒ ์บ์ ๋ฏธ์ค๋ ์ฑ๋ฅ ์ ํ ๋ฐ์
ํด๊ฒฐ ๋ฐฉ๋ฒ:
- ์บ์ ๋ผ์ธ ํจ๋ฉ(Padding)์ ๋ฃ์ด์ ๋ถ๋ฆฌ์ํด
๋ฐ์ดํฐ๋ฅผ ์๋ก ๋ค๋ฅธ ์บ์๋ผ์ธ์ ๋ฐฐ์นํ๋ ๊ฒ
[StructLayout(LayoutKind.Explicit, Size = 128)] // 64๋ฐ์ดํธ ์ด์์ผ๋ก ๋ถ๋ฆฌ
public struct PaddedInt
{
[FieldOffset(64)] public int value;
}
- StructLayout์ ์ด์ฉํด ๋ฐ์ดํฐ๋ฅผ ์ธ์์ ์ผ๋ก ๋์์ false sharing ๋ฐฉ์ง
- Unity์์๋ ๋น์ทํ ๋ฐฉ์์ ์บ์ ๋ผ์ธ ํจ๋ฉ ๊ธฐ๋ฒ์ด ์ฌ์ฉ๋จ
๋๋ Unity์ Burst/Jobs์์๋:
[StructLayout(LayoutKind.Sequential)]
public struct Counter1 : IJob
{
[NativeDisableParallelForRestriction]
public NativeArray<int> array;
public void Execute()
{
array[0]++;
}
}
-> NativeArray<int>์์ false sharing์ด ๋ ์ ์์ผ๋ index๋ง๋ค 64๋ฐ์ดํธ ์ ๋ ๋์์ ์ ๊ทผํ๊ฑฐ๋ NativeArray<CustomPaddedStruct>๋ฅผ ์ฌ์ฉ
2. Data Alignment (๋ฐ์ดํฐ ์ ๋ ฌ)
์ ์:
CPU๋ ๋ฐ์ดํฐ๊ฐ ํน์ ๋ฐ์ดํธ ๋จ์๋ก ์ ๋ ฌ๋์ด ์์ด์ผ ํจ์จ์ ์ผ๋ก ์ ๊ทผ ๊ฐ๋ฅ
์ ์ ๋ ฌ๋ ๋ฐ์ดํฐ๋ ํ ๋ฒ์ ์ฝ์ ์ ์์, ๊ทธ๋ ์ง ์์ผ๋ฉด ์ฌ๋ฌ ๋ฒ ์ฝ๊ฑฐ๋ ๋๋ ค์ง
struct A {
byte a;
int b;
}
- ์ ๊ตฌ์กฐ์ฒด์์ byte a๋ 1๋ฐ์ดํธ์ง๋ง, int b๋ 4๋ฐ์ดํธ ์ ๋ ฌ์ด ํ์ํจ -> ์ฌ์ด์ 3๋ฐ์ดํธ ํจ๋ฉ์ด ์๋ ์ฝ์
๋จ
Unity์์ ์ฃผ์ํ ์ :
- IComponentData ๊ตฌ์กฐ์ฒด๋ ๊ฐ๋ฅํ ํ 4,8,16๋ฐ์ดํธ ๋จ์๋ก ์ ๋ ฌํ๋ ๊ฒ ์ข์
- float3๋ 16๋ฐ์ดํธ ์ ๋ ฌ๋จ -> ๋ค์ int๊ฐ ์ค๋ฉด ๋ถํ์ํ ์ ๋ ฌ๋น์ฉ ๋ฐ์ ๊ฐ๋ฅ
์ ๋ ฌ ์ต์ ํ
// ๋์ ์
struct Bad {
public int a;
public byte b;
public float3 c;
}
// ์ข์ ์
struct Good {
public float3 c;
public int a;
public byte b;
}