거짓 공유

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

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

 예시 
#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 기사: 거짓 공유 제거하기
 자바에서 거짓 공유 제거 시 주의사항

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