거짓 공유

컴퓨터 과학에서 **거짓 공유**(False sharing)는 분산된 일관성 있는 캐시를 가진 시스템에서 캐싱 메커니즘에 의해 관리되는 가장 작은 리소스 블록 크기에서 발생할 수 있는 성능 저하 사용 패턴이다. 시스템 참여자가 다른 파티에 의해 변경되지 않는 데이터에 주기적으로 액세스하려고 하지만, 해당 데이터가 변경되는 데이터와 캐시 블록을 공유하는 경우 캐싱 프로토콜은 논리적 필요성 부족에도 불구하고 첫 번째 참여자가 전체 캐시 블록을 다시 로드하도록 강제할 수 있다.page=537 캐싱 시스템은 이 블록 내 활동을 인지하지 못하고 첫 번째 참여자가 리소스의 진정한 공유 액세스에 필요한 캐싱 시스템 오버헤드를 부담하도록 강제한다.
# 다중 프로세서 CPU 캐시
이 용어의 가장 일반적인 사용은 최신 다중 프로세서 CPU 캐시에서 발생하며, 여기서 메모리는 일부 작은 2의 거듭제곱 워드 크기(정렬된, 연속된 바이트 64개 등)의 라인으로 캐시된다. 두 프로세서가 단일 라인에 저장될 수 있는 동일한 메모리 주소 영역의 독립적인 데이터에서 작동하는 경우, 시스템의 캐시 일관성 메커니즘은 모든 데이터 쓰기에서 전체 라인을 버스 또는 인터커넥트를 통해 강제 전송하여 시스템 대역폭을 낭비하는 것 외에도 메모리 지연을 유발할 수 있다. 일부 경우, 거짓 공유를 제거하면 성능이 몇 배 향상될 수 있다.access-date=11 July 2021 거짓 공유는 자동 동기화 캐시 프로토콜의 본질적인 특징이며 분산 파일 시스템 또는 데이터베이스와 같은 환경에서도 존재할 수 있지만, 현재는 RAM 캐시로 한정된다.
# 예시
#include #include #include #include #include #include #include using namespace std; using namespace chrono; #if defined(__cpp_lib_hardware_interference_size) // default cacheline size from runtime constexpr size_t CL_SIZE = hardware_constructive_interference_size; #else // most common cacheline size otherwise constexpr size_t CL_SIZE = 64; #endif int main() { vector threads; int hc = jthread::hardware_concurrency(); hc = hc <= CL_SIZE ? hc : CL_SIZE; for( int nThreads = 1; nThreads <= hc; ++nThreads ) { // synchronize beginning of threads coarse on kernel level latch coarseSync( nThreads ); // fine synch via atomic in userspace atomic_uint fineSync( nThreads ); // as much chars as would fit into a cacheline struct alignas(CL_SIZE) { char shareds[CL_SIZE]; } cacheLine; // sum of all threads execution times atomic_int64_t nsSum( 0 ); for( int t = 0; t != nThreads; ++t ) threads.emplace_back( [&]( char volatile &c; ) { coarseSync.arrive_and_wait(); // synch beginning of thread execution on kernel-level if( fineSync.fetch_sub( 1, memory_order::relaxed ) != 1 ) // fine-synch on user-level while( fineSync.load( memory_order::relaxed ) ); auto start = high_resolution_clock::now(); for( size_t r = 10'000'000; r--; ) c = c + 1; nsSum += duration_cast( high_resolution_clock::now() - start ).count(); }, ref( cacheLine.shareds[t] ) ); threads.resize( 0 ); // join all threads cout << nThreads << ": " << (int)(nsSum / (1.0e7 * nThreads) + 0.5) << endl; } } 이 코드는 거짓 공유의 효과를 보여준다. 이 코드는 시스템의 물리적 스레드 수까지 한 스레드부터 시작하여 스레드 수를 증가시킨다. 각 스레드는 캐시 라인의 한 바이트를 순차적으로 증가시키는데, 이 캐시 라인은 전체적으로 모든 스레드에서 공유된다. 스레드 간 충돌 수준이 높을수록 각 증분 작업에 더 오랜 시간이 걸린다. 다음은 16코어 32스레드를 가진 Zen4 시스템에서의 결과이다.

1: 1
2: 4
3: 6
4: 9
5: 11
6: 13
7: 15
8: 17
9: 16
10: 18
11: 21
12: 25
13: 29
14: 35
15: 39
16: 41
17: 43
18: 44
19: 48
20: 49
21: 51
22: 53
23: 58
24: 61
25: 68
26: 75
27: 79
28: 82
29: 85
30: 88
31: 91
32: 94

위에서 볼 수 있듯이, 해당 시스템에서 공유 캐시 라인에 대한 증분 작업을 완료하는 데 최대 100나노초가 걸릴 수 있으며, 이는 이 CPU에서 약 420클럭 사이클에 해당한다.
# 완화
거짓 공유의 효과를 완화하는 방법이 있다. 예를 들어, CPU 캐시에서의 거짓 공유는 변수를 재배열하거나 변수 사이에 패딩(사용되지 않는 바이트)을 추가하여 방지할 수 있다. 그러나 이러한 프로그램 변경 중 일부는 객체의 크기를 증가시켜 메모리 사용량이 높아질 수 있다. 컴파일 시 데이터 변환 또한 거짓 공유를 완화할 수 있다.doi-access=free 하지만 이러한 변환 중 일부는 항상 허용되지 않을 수 있다. 예를 들어, C++23 C++ 프로그래밍 언어 표준 초안은 데이터 멤버가 이후 멤버가 더 높은 주소를 갖도록 배치되어야 한다고 규정한다.access-date=2021-07-11 거짓 공유를 탐지하는 도구가 있다.access-date=2021-08-08isbn=9781450349826 또한 실행 중인 프로그램에서 거짓 공유를 탐지하고 복구하는 시스템도 있다. 그러나 이러한 시스템은 일부 실행 오버헤드를 발생시킨다.isbn=9781450319942pages=3–18