3주차 실습을 진행하던 중, TimeSheet
클래스의 GetTotalTime
, GetAverageTime
, GetStandardDeviation
메서드를 구현하고 있었습니다.
각 메서드는 객체에 저장된 시간 데이터들의 합, 평균, 표준편차를 계산합니다.
이들은 객체 내부의 상태를 읽기만 할 뿐 변경시키지 않기에, const
메서드로 만드는 것이 타당합니다.
첫 구현에서는 메서드 호출 시 동일한 결과를 매번 새로 계산 하기 때문에 비효율적이었습니다.
계산한 결과값들은 사용자가 AddTime
을 호출하여 데이터를 추가하지 않는 한 유효하기에, 결과값을 캐싱하여 반복 호출 시 처리 시간을 줄이고자 했습니다.
아래 코드와 같이 메서드 별 캐시된 값을 저장할 변수(mCacheTotal
)와, 캐시된 결과값이 유효한지를 나타내는 bool
형 변수(mbCacheTotalValid
)를 멤버 변수로 선언해 구현할 수 있습니다.
캐시가 무효화되는 시점은 AddTime
에서 성공적으로 시간 데이터가 추가될 때입니다.
int TimeSheet::GetTotalTime() const
{
if (mbCacheTotalValid)
{
return mCacheTotal;
}
int totalTime; // totalTime 계산 코드 생략
mCacheTotal = totalTime;
mbCacheTotalValid = true;
return totalTime;
}
bool TimeSheet::AddTime(int hours)
{
// 예외 상황 처리 코드 및 데이터 추가 코드 생략
mbCacheTotalValid = false;
return true;
}
위 구현 원리를 따를 경우, 배경에서 언급한 const
메서드와 모순되어 문제가 생깁니다.
왜냐하면 const
메서드는 내부 상태를 변경할 수 없으므로, 결과값을 캐시 멤버 변수에 쓰지도 valid 멤버 변수를 바꿀 수도 없습니다.
이때 아래 코드와 같이 mutable
키워드를 사용해 멤버 변수를 선언하면, const
메서드 내에서도 멤버 변수를 수정할 수 있습니다.
mutable bool mbCacheTotalValid;
mutable int mCacheTotal;
아래 코드를 실행시켜 반복 호출에 따른 캐싱 이전과 이후의 성능 차이를 측정했습니다. 결과는 아래 표와 같으며 수행 시간의 단위는 clock입니다.
첫 호출에는 비슷한 시간이 걸리지만, 이후 호출에서는 캐싱을 통해 빠르게 동작하는 결과를 보여주고 있습니다.
호출 횟수 | 캐싱 X | 캐싱 O |
---|---|---|
1 | 963 | 947 |
2 | 950 | 0 |
3 | 949 | 0 |
4 | 947 | 0 |
5 | 947 | 0 |
#include <assert.h>
#include <iostream>
#include <random>
#include "TimeSheet.h"
using namespace std;
using namespace lab3;
int main()
{
const int NUM_ENTRIES = 100000000;
// 더미 데이터 삽입
TimeSheet s("foo", NUM_ENTRIES);
for (size_t i = 0; i < NUM_ENTRIES; ++i)
{
int hours = rand() % 10 + 1;
s.AddTime(hours);
}
// 5번의 메서드 호출 간 수행 시간(clock) 측정하기
for (size_t i = 0; i < 5; ++i)
{
clock_t start = clock();
float _sd = s.GetStandardDeviation();
cout << "#" << i << ": " << clock() - start << endl;
}
return 0;
}