데이터 지향 설계는 프로그램을 객체나 기능 중심이 아니라, 데이터의 구조와 배치 방식 중심으로 설계하는 방법론이다. CPU 캐시 효율, 메모리 접근 패턴 최적화, 병렬성 향상을 통해 성능을 극대화한다.
<aside> 💡
객체 지향 설계는 무엇을 할지에 초점을 맞추는 반면, 데이터 지향 설계는 데이터를 어떻게 다룰지에 초점을 맞춤
</aside>
CPU와 메모리 속도 차이
CPU 성능이 매년 크게 향상되었지만, 메모리 접근 속도는 훨씬 느리게 발전
→ CPU는 연산 준비가 되었지만, 필요한 데이터가 메모리에서 캐시로 오지 않아 메모리 병목 현상 발생
OOP의 메모리 비효율
OOP는 객체 단위로 설계하므로, 서로 다른 타입의 데이터가 메모리상에 흩어져 저장
<aside> 💡
객체 내부 메모리 구조
class Player {
int score; // 4 byte
double hp; // 8 byte
boolean alive; // 1 byte
}
참조 타입의 분리 저장
class User {
String name; // 참조: 힙 어딘가에 저장된 문자열 객체
int age; // 값: User 객체 내부에 저장
}
상속 구조와 가상 함수 테이블
다형성을 위해 객체는 클래스 정보와 메서드 주소를 담는 포인터가 포함
객체 하나를 읽을 때
여러번 메모리 점프가 필요
</aside>
→ 캐시 미스 빈도가 높아져 성능 저하 발생
대규모 데이터 처리 필요성
게임 엔진, 시뮬레이션, 머신 러닝 등 대량의 데이터를 처리하는 분야에서 CPU 캐시 친화적인 설계의 필요성 증가
데이터를 사용 패턴에 따라 묶고 재배치
구조체 배열과 같은 타입의 데이터들이 연속적으로 배치되어 캐시 효율 증가
작업 단위로 데이터 정렬
데이터는 사용되는 순서에 맞춰 배치 → 불필요한 데이터 방지
CPU 친화적 설계
L1/L2/L3 캐시에 최대한 적합하도록 데이터 레이아웃 설계
<aside> 💡
CPU 친화적 설계?
int size = 100;
var data = new int[size];
var result = 0;
var round = 0;
for (int i = 0; i < size; i++) {
round += 1;
result += data[i];
}
int size = 1600;
var data = new int[size];
var result = 0;
var round = 0;
for (int i = 0; i < size; i+=16) {
round += 1;
result += data[i];
}
두코드는 모두 반복문을 100번 실행한다.
그러나 size가 100인 코드가 size가 1600인 코드보다 수행 시간이 적다.
해당 원인은 CPU의 캐시와 연관이 있다. CPU 캐시는 메인 메모리와의 속도차이를 완화하기 위한 저정소로 CPU 캐시를 사용하면 데이터나 명령어를 더 빠르게 읽어올 수 있다.
CPU 캐시는 L1, L2, L3로 구성된 것이 일반적으로 각 계층에 대한 설명은 다음과 같다.
L1
CPU에 가장 가까운 캐시로, 속도가 가장 빠르고 가장 용량이 적다. 자주 사용되는 데이터와 명령어를 저장할 수 있으며, 데이터 캐시와 명령어 캐시로 나누어져 있다.
L2
L1보다 용량이 큰 대신 속도가 느린 캐시로 L1에서 찾지 못한 데이터와 명령어를 저장한다. L2 캐시는 CPU 내부에 있거나 외부에 있을 수 있다.
L3
L2보다 크리고 용량이 큰 캐시이다. 여러 CPU 코어가 공유할 수 있다.
참고로 각 CPU > REGISTER > L1 > L2 > L3 > DRAM > HARD DISK 순으로 속도가 빠르고 크기가 작다. 즉, CPU 가 제일 크기가 작고 속도가 빠르다.
CPU는 RAM의 데이터에 직접 엑세스할 수 없다. 대신 RAM에 데이터를 복사한 캐시에 데이터에 접근한다. 캐시에 데이터가 없으면 캐시 미스가 발생하고, CPU는 캐시로 데이터가 복사되길 기다린다.
위예제에서 size가 1600인데 캐시는 i를 16씩 증가 시켯으며, int는 4바이트이므로 L1의 사이즈를 64바이트로 생각해보자. 그럼 size가 100인 코드는 7번의 캐시 미스가 나지만 size가 1600인 캐시는 100번의 캐시 미스가 발생한다. 즉, 캐시 미스의 차이로 성능 차이가 나게되어 size가 1600인 코드가 더 느린 성능을 가진다.
정리하면, CPU 친화적 설계란 말은 CPU 캐시에 올리기 쉽도록 데이터를 연속적으로 저장하는 것이다. 이는 CPU N캐시 미스를 줄여 성능을 개선시키는데 도움이 된다.
</aside>