cpp 코드를 통해 캐시 튕김 문제를 확인해보자.
첫 번째 코드
atomic<int> a;
void threadf()
{
for (int i = 0; i < 500000000; i++)
{
++a;
}
}
void run()
{
thread t1 = thread(threadf);
thread t2 = thread(threadf);
t1.join();
t2.join();
}
두 번째 코드
atomic<int> a;
void run()
{
for (int i = 0; i < 1000000000; i++)
{
++a;
}
}
위 두 가지 코드를 비교하자면, 멀티코어에서 캐싱을 고려하지 않았을 때 첫번째 코드가 두번째 코드보다 두 배 빨라야 할 것처럼 보인다.
그러나 실제 실행 시, 첫번째 코드의 실행시간은 16초, 두번째 코드의 실행시간은 8초로, 두번째 코드가 더 빠르다.
다음 두 코드를 통해 거짓 공유(false sharing) 문제를 확인해보자.
전역변수
struct data
{
int a;
int b;
}
struct data global_data;
첫 번째 코드
void add_a()
{
for (int i = 0; i < 500000000; i++)
{
++global_data.a;
}
}
void add_b()
{
for (int i = 0; i < 500000000; i++)
{
++global_data.b;
}
}
void run()
{
thread t1 = thread(add_a);
thread t2 = thread(add_b);
t1.join();
t2.join();
}
두 번째 코드
void run()
{
for (int i = 0; i < 500000000; i++)
{
++global_data.a;
}
for (int i = 0; i < 500000000; i++)
{
++global_data.b;
}
}
위 두가지 코드에서는 캐시 튕김 문제가 발생하지 않는다. 따라서 첫 번째 코드가 두 번째 코드보다 두 배 빠를 것으로 예상된다.
그러나 실제로는 첫 번째 코드는 3초, 두 번째 코드는 2초로, 두 번째 코드가 더 빠르다.
이는 거짓 공유(false sharing) 문제로 인한 것이다.
두 스레드가 변수를 직접 공유하지는 않지만, 높은 확률로 두 변수가 같은 캐시라인에 있을 것이다. 따라서 이 때문에 캐시 무효화가 발생한다.
이런 경우, 두 변수 사이에 사용하지 않는 변수를 채워넣으면 해결 될 수 있다.
struct data
{
int a;
int arr[16];
int b;
}