CPU의 캐시라인이란?

 CPU가 메모리로부터 데이터를 가져올 때는 바이트 단위로 가져오지 않고 캐시라인을 가득 채울 만큼의 데이터를 

 가져오는 것을 말합니다. (메모리의 페이징 기법과 비슷합니다.)

 캐시라인의 크기는 32, 64, 128바이트(CPU에 따라 다름) 구성되며 해당 사이즈 경계로 정렬되어 있습니다.

CPU-Z로 확인한 CPU 정보

 필자의 컴퓨터의 경우 64바이트의 캐시라인을 사용하고 있습니다. 

 캐시 라인을 사용하는 이유는 일반적인 애플리케이션의 경우 인접한 바이트들을 사용하는 경우가 많기 때문에

 CPU의 메모리 접근 횟수를 줄여 성능을 향상 시키기 위함입니다.

멀티 프로세서 환경에서 캐시라인

 캐시라인은 성능의 향상을 위해서 도입되었지만 멀티 프로세서 환경에서는 문제가 될 수 있는 여지가 있습니다.

 다수의 CPU가 동일한 캐시라인을 보유하고 있다고 가정하고 그중 하나의 CPU가 해당 캐시라인을 수정한다면

 다른 CPU들은 해당 캐시라인의 갱신을 어떻게 확인 할 수 있을까요?

 이런 문제를 해결하기 위해서 CPU 설계자는 캐시라인이 수정되면 다른 CPU가 들고 있는 캐시라인을 무효화

 시켜 데이터를 동기화 합니다. 이후 다른 CPU는 해당 캐시라인을 사용하려면 메모리에서 다시 읽어야 합니다.  

 이러한 이유 때문에 캐시라인이 오히려 멀티 프로세서 환경에서는 성능을 저해하는 요인이 될 수 있습니다.

성능을 개선하는 방법

 멀티 프로세서에서 개발을 하는 애플리케이션 개발자는 이러한 속성을 이해해서 애플리케이션에서 사용하는

 데이터를 캐시라인의 크기와 같은 경계라인으로 묶어서 다루는 것이 좋습니다. 

 해당 머신의 CPU의 캐시라인은 어떻게 얻어 올 수 있을까요? WINDOWS API인 GetLogicalProcessorInfomation함수를

 통해서 얻어 올 수 있습니다. MSDN을 참조해서 해당 캐시라인을 가져오는 함수를 만들어 보았습니다.

/**
 * @brief 해당 머신의 CPU 캐시라인을 가져옵니다. 
 * @details WINDOW API 함수인 GetLogicalProcessorInformation를 사용해서 CPU의 캐시라인 정보를 얻어 옵니다.
 * @param std::map<BYTE,WORD> & mapCpuCacheLineSize cpu 캐시라인 정보를 저장합니다. out param
 * @return BOOL 성공시 TRUE, 실패시 FALSE 반환
 * @author woong20123
 * @date 2020-02-15
 */
BOOL GetCpuCashLineSize(std::map<BYTE,WORD> & mapCpuCacheLineSize)
{
    PSYSTEM_LOGICAL_PROCESSOR_INFORMATION buffer = NULL;
    PSYSTEM_LOGICAL_PROCESSOR_INFORMATION ptr = NULL;
    DWORD returnLength = 0;

    BOOL bDone = FALSE;
    do
    {
        DWORD rc = GetLogicalProcessorInformation(buffer, &returnLength);
        if (FALSE == rc)
        {
            if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
            {
                if (buffer)
                    free(buffer);
                // returnLength에 buffer를 할당한 사이즈를 전달 받습니다.
                buffer = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)malloc(returnLength);
                // buffer를 할당하고 다시 GetLogicalProcessorInformation 호출! 
                if (NULL == buffer)
                {
                    return FALSE;
                }
            }
            else
            {
                return FALSE;
            }
        }
        else
        {
            bDone = TRUE;
        }
    } while (!bDone);


    if (buffer)
    {
        ptr = buffer;
        DWORD byteOffset = 0;
        PCACHE_DESCRIPTOR Cache;
		
        // 전달받은 버퍼를 순회하면서 Cache정보한 사용합니다
        while (byteOffset + sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) <= returnLength)
        {
            switch (ptr->Relationship)
            {
            	// 타입이 캐시 일 때만 정보를 가져옵니다. 
                case RelationCache:
                    Cache = &ptr->Cache;
                    if (mapCpuCacheLineSize.end() == mapCpuCacheLineSize.find(Cache->Level))
                    {
                        mapCpuCacheLineSize.insert(std::map<BYTE, WORD>::value_type(Cache->Level, Cache->LineSize));
                    }
                    break;
            }
            
            byteOffset += sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
            ptr++;
        }
        free(buffer);
        return TRUE;
    }

    return FALSE;
}
    // CPU cache Line을 얻어오는 함수 호출 방법 
    std::map<BYTE, WORD> mapCpuCacheLineSize;
    if (FALSE == GetCpuCashLineSize(mapCpuCacheLineSize))
    {
        cout << "FALSE == GetCpuCashLineSize GetLastError = " << GetLastError() << endl;
    }
    else
    {
        std::for_each(mapCpuCacheLineSize.begin(), mapCpuCacheLineSize.end(),
            [](std::map<BYTE, WORD>::value_type& val)
        {
            cout << "CacheLevel = " << (WORD)val.first << " wCPUCacheLineSize = " << val.second << endl;
        });
        
    }

결과값

 실행해보니 CPU-Z에서 나온 값과 동일하게 나온 것을 확인하였습니다.  

 이렇게 획득한 CPU캐시라인을 기반으로 데이터의 경계라인을 나눌 때는 읽기 전용의 데이터와

 읽고 쓰기가 빈번한 데이터를 구분하는 것이 좋습니다. 

const WORD kUSERNAME_COUNT = 100;

struct stUserInfo
{
    DWORD    dwUserSn;                   // 거의 읽기 전용으로 사용
    DWORD    dwUserMoney;                // 빈번하게 수정됨
    wchar_t szUserName[kUSERNAME_COUNT]; // 거의 읽기 전용으로 사용
    WORD    wChannelNo;                  // 빈번하게 수정됨
};

 위의 코드를 보시면 멀티 프로세서 환경에서 비효율적으로 설계된 stUserInfo 구조체를 확인 할 수 있습니다.

 dwUserSn과 szUserName은 읽기 전용이기 때문에 거의 수정되지 않습니다. 하지만 dwUserMoney와 wChannelNo는

 자주 수정되기 때문에 같은 캐시라인에 들어 있다면 무효화될 가능성이 높습니다. 

 읽기 전용 데이터들만 캐시라인을 구성하면 읽기 전용 캐시라인이 무효화되는 횟수가 줄어들어 

 멀티 프로세서 환경에서 효율적이게 됩니다. 

#define CACHE_ALIGN 64
struct __declspec(align(CACHE_ALIGN)) stUserInfo
{
    DWORD    dwUserSn;                   // 거의 읽기 전용으로 사용
    wchar_t szUserName[kUSERNAME_COUNT]; // 거의 읽기 전용으로 사용

    // 빈번하게 수정될 수 잇는 변수들은 다른 캐시라인으로 들어가도록 합니다. 
    __declspec(align(CACHE_ALIGN))
    DWORD    dwUserMoney;                // 빈번하게 수정됨
    WORD    wChannelNo;                  // 빈번하게 수정됨
};

/**

 위의 코드는 C/C++ 컴파일러가 제공하는 __declspec(align(크기))를 사용해서 읽기 전용 데이터와 읽고 쓰기 데이터를

 다른 캐시라인이 되도록 개선된 구조체를 확인 할 수 있습니다.

'c++ > windows' 카테고리의 다른 글

[Window c++] 슬림 리더-라이터 락 (SWRLock)  (1) 2020.02.16
[Window c++] 크리티컬 섹션  (0) 2020.02.16
[window c++] InterLocked 함수들  (2) 2020.02.12
[window c++] 스레드 스케줄링  (0) 2020.02.09
[window c++] 스레드  (0) 2020.02.04

+ Recent posts