NeuroWhAI의 잡블로그

C++로 재현한 가짜 공유(거짓 공유, False sharing) 문제 및 설명 본문

개발 및 공부/설계

C++로 재현한 가짜 공유(거짓 공유, False sharing) 문제 및 설명

NeuroWhAI 2018. 9. 24. 15:34


가짜 공유캐시 코히런스 때문에 시스템이 실제로 공유되고 있지 않은 캐시 데이터를 동기화하는 행위 또는 그로 인해 발생하는 성능 하락을 말합니다.

얼마전 에서 읽었는데 충격적인지라 기억에 남습니다.
그냥 바로 코드를 봅시다.
(주의: 하드웨어 환경에 따라 결과가 상이할 수 있음)

코드 및 결과 : https://wandbox.org/permlink/Nr8F51OgbILBiIUw
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

using int64 = long long int;

constexpr int64 NUMBERS = 1000000000LL;
volatile int64 num1 = 0;
volatile int64 num2 = 0;

void job1()
{
    for (int64 i=0; i<NUMBERS; ++i)
    {
        num1 += 1LL;
    }
}

void job2()
{
    for (int64 i=0; i<NUMBERS; ++i)
    {
        num2 += 1LL;
    }
}

int main()
{
    auto beginTime = chrono::high_resolution_clock::now();

    std::thread task1(job1);
    std::thread task2(job2);
    
    task1.join();
    task2.join();
    
    auto endTime = chrono::high_resolution_clock::now();
    chrono::duration<double> elapsedTime = endTime - beginTime;
    
    cout << "num1: " << num1 << endl;
    cout << "num2: " << num2 << endl;
    
    cout << elapsedTime.count() << "s" << endl;

    return 0;
}
num1: 1000000000
num2: 1000000000
8.41568s
스레드 두 개를 만들어서 각 스레드가 병렬로 독립적인 각 변수의 값을 1씩 더해가는 코드인데 뮤텍스 같은 동기화가 필요없는 완벽한 병행성을 가지는 코드입니다.
Wandbox에서 실행하면 대략 7초에서 10초 정도 걸리네요.

그런데 컴파일러나 실행 환경에 따라서 가짜 공유 문제가 발생할 수 있으며 위 코드에서도 발생했다고 가정하고 최적화(?)를 해보겠습니다.

코드 및 결과 : https://wandbox.org/permlink/8Wckr5r9lOtMWLxF
...
constexpr int CACHE_LINE_SIZE = 64;

constexpr int64 NUMBERS = 1000000000LL;
alignas(CACHE_LINE_SIZE) volatile int64 num1 = 0;
alignas(CACHE_LINE_SIZE) volatile int64 num2 = 0;
...
바뀐건 num1과 num2의 메모리 상에서의 정렬(배치) 방식을 64바이트[각주:1] 단위로 만든 것 뿐입니다.
쉽게 말해서 두 변수의 메모리 상 거리를 좀 더 띄어둔 것입니다.
결과는 어떨까요?
num1: 1000000000
num2: 1000000000
2.98661s
!
무려 2.8배나 빨라졌습니다.
실행할 때마다 편차는 있겠지만 적어도 두 배는 빨라졌다는 거죠.

이게 바로 가짜 공유 문제입니다.


다들 CPU에 캐시란 놈이 달려있다는 것과 L1, L2, L3처럼 나눠져 있고 L1, L2 정도는 각 코어마다 달려있다는 것도 아실겁니다.
프로그램이 동작할때 데이터를 메모리에서 읽는다고 말하지만 실제론 캐시에서 먼저 찾고 없으면 그 다음 캐시, 마지막엔 실제 메모리까지 가서 캐시에 불러와 읽잖아요?
그럼 바로 떠오르는 문제가 만약 코어1 캐시의 변수A 값과 코어2 캐시의 변수A 값이 달라진다면 어떻게 되느냐?일겁니다.

그래서 캐시 코히런스가 있습니다.
캐시 일관성을 유지하는 방법엔 여러가지가 있겠지만 대충 코어들끼리 소통하면서 각 캐시를 동기화 한다고 생각하시면 됩니다.
가장 무식한 동기화 방법은 이 데이터를 가진 다른 캐시를 모두 무효화시켜서 다시 불러오게끔 하는겁니다.
이 동기화에 비용(클럭=시간)이 들거라는건 자명하죠.

여기서 잠깐, 캐시는 데이터 덩어리라고 보면 되는데 변수A를 읽는다고 캐시에 변수A만 불러오는게 아니라 인접한 곳의 데이터까지 캐시에 가져오게 됩니다.
만약 인접한 곳에 변수B가 있다면 역시 가져오게 되죠.
이게 바로 처음에 말했던 "실제로 공유되고 있지 않은 캐시 데이터"입니다.
코어1의 변수A를 사용하기 위해 캐시에 불러왔지만 변수B까지 딸려와 불러오게 된 것이고
코어2는 변수B를 사용하기 위해 캐시에 불러왔지만 변수A까지 딸려오게 된 것이죠.

이러고 코어1이 변수A에 값을 쓴다면 코어2의 캐시에도 동기화를 해줘야 할테니 동기화가 일어나고
코어2가 변수B에 값을 쓰면 코어1의 캐시에도 동기화를 해주고 또.. 계속 계속...
이게 바로 가짜 공유 문제입니다!

그래서 num1과 num2의 거리를 띄워줬을때 문제가 해결됬던 것이죠.
근데 앞서 말했지만 컴파일러나 실행 환경에 따라 아무런 차이가 없을 수도 있습니다.
IdeOne에서 해봤을 때는 차이가 없더라구요.

끝이고 뭔가 틀린 부분도 있겠지만 대충 이런 느낌이구나라고만 생각해주시면 감사합니다 ㅎ..




  1. C++17부터는 new 헤더에 정의된 상수에서 얻을 수도 있습니다. (https://en.cppreference.com/w/cpp/thread/hardware_destructive_interference_size) [본문으로]


Comments