본문 바로가기
Programming/Server

캐시에 의한 효율성

by 작은별하나 2012. 1. 25.
반응형

캐시에 의한 효율성

 

서버 프로그램을 하면서 가장 고려를 해야 하는 부분은 메모리에 의한 병목현상(bottleneck)입니다. 이러한 병목현상을 줄이기 위해서 최소한의 메모리 복사가 이루어져야 하는 것은 당연하겠지만, 자료구조를 비롯해서 캐시를 잘 활용해야 합니다. 서버로 사용하는 CPU는 캐시 메모리가 큰 것을 일반적으로 사용합니다. 그만큼 캐시에 대한 이해도가 높아야 하겠죠.

 

캐시는 CPU와 메모리 사이에 위치하면서, 느린 메모리 속도를 보완해주는 역할을 합니다.

 

 

최근의 CPU 캐시는 일반적으로 L1, L2, L3 로 나누어집니다. L1 캐시가 CPU와 가장 근접해 있는 것이라고 생각하시면 됩니다. L2는 L1을 보완해주는 역할을 주로 합니다. L3 캐시는 L1, L2와 달리 코어간 공유 캐시로 사용됩니다.

 

캐시는 라인(Line)이라는 단위로 관리가 됩니다. 캐시라인은 일정한 크기로 나누어집니다. 일반적으로 32Byte, 64Byte, 128Byte 등으로 나누어집니다. 이 캐시라인은 캐시 일관성(Cache coherence)에 매우 중요한 역할을 합니다. 한 바이트를 읽어도 캐시라인 단위로 메모리부터 읽어오며, 데이터를 메모리에 쓸 때 역시 캐시라인 단위로 쓰여지게 됩니다. 하나의 캐시라인에 있는 내용은 동시에 쓰여지기 때문에 큰 문제가 없습니다만, 구조체 등에서 pack(1)을 사용할 경우 심각한 문제를 발생시킬 수도 있습니다.

 

캐시에 대한 이해는 멀티프로세스 프로그램 작성에 꼭 필요하다고 생각합니다. 이미 속도의 한계에 부딪친 CPU 입장에서는 멀티코어(Multi-core), 멀티프로세서(Multi-processor)로 발전할 수밖에 없겠죠. 이제 서버뿐 아니라 클라이언트도 멀티프로세스 프로그램이 필요한 시점이 되고 있습니다. 다음 그림은 인텔자료 중 QPI에 대한 것입니다. 멀티프로세스로 가다 보면 가장 중요한 부분이 메모리와 관련된 것이죠. 코어 안에서 동작하는데 필요한 레지스터들은 모두 코어 독립적일 수밖에 없습니다. 멀티프로세스는 잘 쓰면 이득, 잘 못 쓰면, 쓰지 않는 것만 못 하다는 말이 있습니다.




 

다음의 프로그램은 캐시의 효율성을 해치는 경우와 아닌 경우로 나누어서 성능을 측정하는 프로그램입니다. 성능을 측정할 때에는 다른 프로그램을 수행하지 않고 측정하는 것이 좋겠죠. 특히 캐시의 경우에는 다른 프로그램에서 캐시를 많이 쓰고 있다면, 그만큼 성능 측정에 영향을 미칠 가능성이 많습니다. 모든 것을 고려한다 해도 이 프로그램의 결과는 너무나 확연해서, 캐시에 대한 성능의 차이를 잘 느낄 수가 있습니다.

[캐시 테스트 프로그램] 

#include "stdafx.h"
#include <Windows.h>

#define    TESTNUM    10000

void InitImage(char *image);
void InvertImage1(char *image);
void InvertImage2(char *image);
void InvertImage3(char *image);

int main()
{
    char *image = new char[512*512];
    DWORD t;

    InitImage(image);
    t = GetTickCount();
    for( int i = 0 ; i < TESTNUM ; i++ )
        InvertImage1(image);
    printf("Test 1 : %.2f\n", (GetTickCount()-t)/1000.0f);

    InitImage(image);
    t = GetTickCount();
    for( int i = 0 ; i < TESTNUM ; i++ )
        InvertImage2(image);
    printf("Test 2 : %.2f\n", (GetTickCount()-t)/1000.0f);

     InitImage(image);
    t = GetTickCount();
    for( int i = 0 ; i < TESTNUM ; i++ )
        InvertImage3(image);
    printf("Test 3 : %.2f\n", (GetTickCount()-t)/1000.0f);

    delete[] image;

    return 0;
}

void InitImage(char *image)
{
    for( int y = 0 ; y < 512 ; y++ )
    {
        for( int x = 0; x < 512 ; x++ )
        {
            *(image+y*512+x) = (x+y)&255;
        }
    }
}

void InvertImage1(char *image)
{
    for( int y = 0 ; y < 512 ; y++ )
    {
        for( int x = 0; x < 512 ; x++ )
        {
            *(image+y*512+x) ^= (char)-1;
        }
    }
}

void InvertImage2(char *image)
{
    for( int x = 0 ; x < 512 ; x++ )
    {
        for( int y = 0; y < 512 ; y++ )
        {
            *(image+y*512+x) ^= (char)-1;
        }
    }
}

void InvertImage3(char *image)
{
    for( int y = 0 ; y < 512 ; y++ )
    {
        for( int x = 0; x < 512 ; x++, image++ )
        {
            *image ^= (char)-1;
        }
    }
}


 

다음은 이 프로그램을 수행했을 때의 결과입니다. 첫번째는 캐시를 잘 활용한 경우입니다. 캐시는 결국 연속 메모리에 최적화되었다고 할 수 있습니다. 어떤 캐시 방법을 사용하든지 캐시는 연속된 메모리에 대해서 가장 잘 동작합니다. 두번째는 고의로 캐시를 잘 활용치 못 하도록 한 것입니다. 성능이 무려 3배나 좋지 않게 나온 것을 알 수 있습니다. 세번째는 메모리 계산을 단순 증가로 바꾼 경우입니다. 당연히 세번째의 결과가 첫번째와 비교해서 잘 나와야 하겠지만, 이 결과는 너무나 미미해서 오차범위 안에서 좋게 나올 때도 나쁘게 나올 때도 있습니다.

 

 

이상과 같이 캐시 미스에 의한 성능에 대해서 이야기를 마칩니다. 다음에는 캐시 라인에 걸친 데이터 때문에 생기는 데이터가 불안정해지는 경우에 대해서 이야기하도록 하겠습니다.

댓글