배경

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;
}

mutable 멤버 변수

위 구현 원리를 따를 경우, 배경에서 언급한 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;
}