멀티스레드 환경에서의 캐시 라인에 대한 주의사항
OS는 프로세서의 정보를 얻을 수 있는 API를 제공한다.
윈도우즈의 경우 다음과 같이 작성해보자.
#include <iostream>
#include <Windows.h>
int main()
{
SYSTEM_LOGICAL_PROCESSOR_INFORMATION* pInfo = nullptr;
unsigned long length = 0;
GetLogicalProcessorInformation(nullptr, &length);
pInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION*)malloc(length);
GetLogicalProcessorInformation(pInfo, &length);
//OS가 알려주는 length는 SYSTEM_LOGICAL_PROCESSOR_INFORMATION의 크기 * N이다.
//즉 SYSTEM_LOGICAL_PROCESSOR_INFORMATION이 여러개라는 것.
//이중에서 RelationCache에 대한 정보를 접근해보자.
//Enum 값 Relation Cache을 오프셋으로 사용해 접근한다.
std::cout << "Cache Line Size : " << (pInfo + RelationCache)->Cache.LineSize << std::endl;
std::cout << "Cache Size : " << (pInfo + RelationCache)->Cache.Size << std::endl;
}
1. 읽으려하는 메모리가 두개의 캐시 라인에 걸치지 않는지 유의한다.
멀티스레드 환경에서, 캐시라인의 크기로 정렬되지 않은 메모리를 읽어오는 상황에 주의한다.
보통은 컴파일러가 알아서 클래스나 구조체의 크기를 캐시라인에 맞게 패딩을 넣어 정렬한다.
바이트 패딩을 사용하지 않으려 한다거나,
혹은 버퍼에 대한 오프셋 연산을 통한 접근이 있다면 다음의 상황이 있는지 유의한다.
다음의 코드를 보도록하자.
#include <iostream>
#include <thread>
int Datas[64 / sizeof(int) * 2] = { 0 , };
//내 머신의 Cache Line Size는 64.
//Line 두개만큼의 메모리를 사용한다.
//즉 128.
int main()
{
std::thread WriterThread([]()
{
for (int i = 0; i < 1000000; ++i)
{
memset(Datas, 0, 128);
memset(Datas, -1, 128);
}
});
std::thread ReaderThread([]()
{
int* pRead = (int*)(reinterpret_cast<char*>(Datas) + 61);
//128 메모리중 61에 위치하도록한다.
//즉, pRead를 읽으면 두개의 캐시라인에 걸치게 하는 것이다.
for (int i = 0; i < 1000000; ++i)
{
int Val = *pRead;
if (-1 != Val && 0 != Val)
{
std::cout << Val << std::endl;
}
}
});
WriterThread.join();
ReaderThread.join();
}
위 코드를 실행하면 -1과 0 이외의 값이 약 10번 정도 나온다.
왜 그런가?
메모리를 읽어올때 캐시라인 단위로 읽어온다.
저렇게 걸치는 곳의 데이터를 읽어오면,
캐시라인 하나에서 값을 읽어오고,
나머지 하나에서 또 읽어온다.
결국 캐시라인을 두번 읽어야 하는 상황에서
나머지 하나의 라인에서 한번 더 읽을때 다른 스레드에서 값을 바꿨다면
두 라인의 영역을 일치 하지 않는 값을 얻어오게 되는 것이다.
2. 캐시라인 무효화에 유의, 용도에 따라 메모리를 구분해 배치 한다.
프로세서는 불필요한 메모리 접근을 하고 싶어 하지 않는다.
읽는 것도, 쓰는 것도.
메모리는 멀리 있다.
그래서 캐시라인 단위로 값을 한번에 가져온다.
비유하자면 보따리로 가져와서, 하나씩 꺼내서 사용한다.
이 상황에서 유의 해야 할 사항이 있다.
두개의 스레드가 별개의 프로세서에서 돌고있다.
공교롭게도 두개의 스레드가 같은 캐시라인에 접근하고 있다고 하자.
이 상황에서 하나의 스레드에서 값을 변경한다면, 나머지 스레드는 어떻게 해야할까?
들고 있는 캐시라인의 값을 무효화하고, 변경된 값으로 가져와야 한다.
보따리 전체를 업데이트 하는 것 이다.
(여기서 유의 할 것은 캐시라인의 특정번지의 값이 특정 레지스터에 담긴 것 까지는 알 수 없다.
레지스터의 값까지 동기화가 일어난다면, 데이터레이스는 존재할 수 없다.)
따라서 값이 빈번하게 수정될 가능성이 큰 데이터와 좀 처럼 수정되지 않는 데이터의 캐시라인을 분리해서
배치하도록 한다.
성능 차이를 보도록 하자.
#include <iostream>
#include <thread>
#include <Windows.h>
#pragma comment(lib,"winmm.lib")
int Datas[64 / sizeof(int) * 2] = { 0 , };
int main()
{
timeBeginPeriod(0);
auto WriteEntry = []()
{
for (int i = 0; i < 1000000000; ++i)
{
Datas[0] += 1;
}
};
auto ReadEntry = []()
{
int TempRead = 0;
for (int i = 0; i < 1000000000; ++i)
{
TempRead = Datas[0];
}
};
unsigned long S = timeGetTime();
std::thread WriteThread(WriteEntry);
std::thread ReadThread(ReadEntry);
ReadThread.join();
unsigned long E = timeGetTime();
//읽는 도중 쓰기 스레드에 의한 캐시 무효화에 실행 속도에 영향을 받은 상태.
//얼마나 걸렸나?
std::cout << "Read & Write Thread Access On Same Cache Line Read Thread Elapsed Time : " << E - S << std::endl;
WriteThread.join();
//Write Thread 종료 시 까지 기다린다.
S = timeGetTime();
std::thread ReadThreadOnlyRead(ReadEntry);
ReadThreadOnlyRead.join();
E = timeGetTime();
//읽는 스레드만 있는 경우.
//얼마나 걸렸나?
std::cout << "Only Read Thread Access On Cache Line Read Thread Elapsed Time : " << E - S << std::endl;
timeEndPeriod(0);
}
위와 같이 읽는 스레드가 쓰기 스레드에 의한 캐시 무효화 영향을 받을때와,
그렇지 않을 때의 성능을 비교해 보았다.
'멀티스레드 프로그래밍' 카테고리의 다른 글
Read/Write 스핀락 구현과 fairness에 대한 이야기 (0) | 2022.02.20 |
---|---|
멀티스레드 프로그래밍 - Data Race (0) | 2021.02.17 |
댓글
이 글 공유하기
다른 글
-
Read/Write 스핀락 구현과 fairness에 대한 이야기
Read/Write 스핀락 구현과 fairness에 대한 이야기
2022.02.20 -
멀티스레드 프로그래밍 - Data Race
멀티스레드 프로그래밍 - Data Race
2021.02.17