거짓 공유

컴퓨터 과학에서 **거짓 공유**(False sharing)는 분산된 일관성 있는 캐시를 가진 시스템에서 캐싱 메커니즘에 의해 관리되는 가장 작은 리소스 블록 크기에서 발생할 수 있는 성능 저하 사용 패턴이다. 시스템 참여자가 다른 파티에 의해 변경되지 않는 데이터에 주기적으로 액세스하려고 하지만, 해당 데이터가 변경되는 데이터와 캐시 블록을 공유하는 경우 캐싱 프로토콜은 논리적 필요성 부족에도 불구하고 첫 번째 참여자가 전체 캐시 블록을 다시 로드하도록 강제할 수 있다. 캐싱 시스템은 이 블록 내 활동을 인지하지 못하고 첫 번째 참여자가 리소스의 진정한 공유 액세스에 필요한 캐싱 시스템 오버헤드를 부담하도록 강제한다.

## 다중 프로세서 CPU 캐시
이 용어의 가장 일반적인 사용은 최신 다중 프로세서 CPU 캐시에서 발생하며, 여기서 메모리는 일부 작은 2의 거듭제곱 워드 크기(정렬된, 연속된 바이트 64개 등)의 라인으로 캐시된다. 두 프로세서가 단일 라인에 저장될 수 있는 동일한 메모리 주소 영역의 독립적인 데이터에서 작동하는 경우, 시스템의 캐시 일관성 메커니즘은 모든 데이터 쓰기에서 전체 라인을 버스 또는 인터커넥트를 통해 강제 전송하여 시스템 대역폭을 낭비하는 것 외에도 메모리 지연을 유발할 수 있다. 일부 경우, 거짓 공유를 제거하면 성능이 몇 배 향상될 수 있다. 거짓 공유는 자동 동기화 캐시 프로토콜의 본질적인 특징이며 분산 파일 시스템 또는 데이터베이스와 같은 환경에서도 존재할 수 있지만, 현재는 RAM 캐시로 한정된다.

## 예시

```cpp
#include <iostream>
#include <thread>
#include <new>
#include <atomic>
#include <chrono>
#include <latch>
#include <vector>

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<jthread> 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<nanoseconds>( 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 캐시에서의 거짓 공유는 변수를 재배열하거나 변수 사이에 패딩(사용되지 않는 바이트)을 추가하여 방지할 수 있다. 그러나 이러한 프로그램 변경 중 일부는 객체의 크기를 증가시켜 메모리 사용량이 높아질 수 있다. 컴파일 시 데이터 변환 또한 거짓 공유를 완화할 수 있다. 하지만 이러한 변환 중 일부는 항상 허용되지 않을 수 있다. 예를 들어, C++23 C++ 프로그래밍 언어 표준 초안은 데이터 멤버가 이후 멤버가 더 높은 주소를 갖도록 배치되어야 한다고 규정한다.

거짓 공유를 탐지하는 도구가 있다. 또한 실행 중인 프로그램에서 거짓 공유를 탐지하고 복구하는 시스템도 있다. 그러나 이러한 시스템은 일부 실행 오버헤드를 발생시킨다.

## 각주

## 외부 링크
* 거짓 공유에 대한 쉬운 이해
* C++ today 블로그, 거짓 공유가 다시 발생하다!
* Dr Dobbs 기사: 거짓 공유 제거하기
* 자바에서 거짓 공유 제거 시 주의사항

분류:컴퓨터 메모리
분류:캐시 일관성