일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- c++
- operator new
- Vector
- Smart Pointer
- 암시적 변환
- virtual function
- 게임
- 반복자
- 언리얼
- 상속
- 영화 리뷰
- 루아
- 메타테이블
- Effective c++
- UE4
- 비교 함수 객체
- exception
- implicit conversion
- 참조자
- resource management class
- lua
- 영화
- 오블완
- 예외
- 티스토리챌린지
- 다형성
- effective stl
- more effective c++
- 스마트 포인터
- reference
Archives
- Today
- Total
스토리텔링 개발자
[More Effective C++] 17. 지연 평가 본문
728x90
항목 17. 효율 향상에 있어서 지연 평가는 충분히 고려해 볼 만하다
지연(to be lazy)
- 효율의 측면에서 볼 때, 최선의 계산은 아무것도 하지 않는 것이다.
- 그렇다면 정말 필요할 때가 되기 전까지 코드 실행을 지연시키는 것이 효율적일 것이다.
- 일반적으로 자주 보게 될만한 일반적인 지연 평가 네 가지를 아래에서 살펴본다.
참조 카운팅(Reference Counting)
class String { ... };
String s1 = "Hello";
String s2 = s1; // String의 복사 생성자를 호출한다.
// String 복사 생성자가 직관적인 구현이라면,
// s2는 s1과 "Hello" 사본을 동시에 들고 있을테지만.. 이는 비용이 크다.
- 직관적인 구현 (즉시 평가(eager evaluation))
- 복사 생성자가 호출되지마자 메모리 할당, 데이터 복사 등을 바로 처리한다.
- 하지만, 복사 생성자는 실행 비용이 있다.
- new 연산자를 통해 힙 메모리를 할당
- strcpy로 s1의 데이터를 s2의 힙 메모리로 복사
- 효율을 고려한 구현 (지연 평가(lazy evaluation))
- s2는 아직 어디에서도 사용되지 않았으므로 아직 자신의 메모리를 가질 필요가 없다.
- 즉, 필요할 때 메모리를 가지도록 한다.
- 위의 사례의 경우 문자열 값이 수정될 때가 바로 더 이상 지연할 수 없을 때라고 할 수 있다.
- 아래와 같이 처리하여 지연 평가한다.
- s2에 s1의 값을 공유하도록 한다.
- 문자열 값이 수정될 때, 수정 대상에 대한 문자열 사본을 할당하도록 한다.
// 이 경우 아직 할당이 필요하지 않다.
cout << s1;
cout << s1 + s2;
// 이 경우 할당이 필요하다.
// s2에 새로운 문자열 할당. 허나 s1은 유지.
s2.convertToUpperCase();
데이터 읽기, 쓰기 구분
String s = "Homer's Iliad"; // s는 참조 카운팅으로 운영되는 문자열이라고 가정.
...
cout << s[3]; // s[3]을 읽기 위해 operator[]를 호출한다.
s[3] = 'x'; // s[3]에 쓰기 위해 operator[]를 호출한다.
- 참조 카운팅으로 유지되는 문자열은 읽기는 효율적이지만, 쓰기에는 불편하다.
- 그렇다면, operator[]가 읽기일 때, 쓰기일 때 다른 동작이 수행되도록 하자.
- 허나, operator[]에서는 읽기인지 쓰기인지 동작을 구별할 방법이 없다.
- 지연 평가 + 프록시 클래스를 사용해서 해결할 수 있다.(항목 30 참조)
데이터 가져오기 지연(Lazy Fetching)
// 계속 지속되어야 하는 '아주 큰' 객체
class LargeObject
{
public:
LargeObject(ObjectID id); // 각각은 유일 식별자를 가진다.
// 다양한 필드값들
const string& field1() const;
int field2() const;
double field3() const;
const string& field4() const;
const string& field5() const;
...
};
void restoreAndProcessObject(ObjectID id)
{
LargeObject object(id); // 객체를 가져온다.
// 헌데... 인스턴스는 매우 크기 때문에 가져오는 데 꽤나 시간이 걸린다.
// 하지만 아래의 경우 데이터를 모두 일일이 읽어오지 않아도 상관이 없다!
// field2 값만 필요하기 때문이다.
if(object.field2() == 0)
{
cout << "Object " << id << ": null field2.\n";
}
}
- 위의 경우 LargeObject 객체를 생성할 때 모든 데이터를 읽지 않도록 지연시킬 수 있다.
- 객체의 인터페이스만 구현하고, 필요한 데이터는 필요할 때마다 데이터베이스에서 뽑아오도록 한다.
- 요구기반(demand paged) 방식의 객체 초기화 방법
class LargeObject
{
public:
LargeObject(ObjectID id);
const string& field1() const;
int field2() const;
double field3() const;
const string& field4() const;
...
private:
ObjectID oid;
mutable string* field1Value;
mutable int* field2Value;
mutable double* field3Value;
mutable string* field4Value;
...
};
LargeObject::LargeObject(ObjectID id) : oid(id), field1Value(nullptr) ....
{}
const string& LargeObject::field1() const
{
if(field1Value == nullptr)
{
DB에서 필드 1의 데이터를 읽고, field1Value가 DB의 값을 포인터로 가리키게 한다.
}
return *field1Value;
}
- 위 코드에서 주의할 점
- 필드 포인터는 멤버 함수 내에서 실제 데이터를 가리키게 수정되어야 한다.
- 하지만.. const 멤버 함수는 '보통의' 클래스 데이터 멤버를 수정할 수 없다.
- 해결 방법
- 변수를 mutable로 선언
- 이 데이터는 어떤 멤버 함수에서도 수정 가능하다는 뜻이다.
- this 흉내내기(fake this)
- 상수성이 제거된 this를 사용하여 해결한다.
-
class LargeObject { public: const string& field1() const; // 인터페이스는 그대로이다. ... private: string* field1Value; ... }; const string& LargeObject::field1() const { // 상수성을 제거한 fake this 생성 LargeObject* const fakeThis = const_cast<LargeObject* const>(this); if(field1Value == nullptr) { // fakeThis가 가리키는 객체는 상수 객체가 아니므로 문제가 발생하지 않는다. fakeThis->field1Value = DB에서 가져온 포인터 } return *field1Value; }
- 멤버 변수를 스마트 포인터로 대체 (항목 28 참조)
- 포인터를 일일이 널로 초기화하고, 사용 전에 널 체크하고.. 등의 번거로운 작업을 제거할 수 있다.
- 변수를 mutable로 선언
표현식 평가 지연(Lazy Expression Evaluation)
template<typename T>
class Matrix { ... }; // 균등 행렬(homogeneous matrices)
Matrix<int> m1(1000, 1000); // 1000 x 1000
Matrix<int> m2(1000, 1000); // 1000 x 1000
...
Matrix<int> m3 = m1 + m2; // m1과 m2를 더한다.
// 여기서 operator+의 동작이 쟁점.
// 1,000,000번의 덧셈이 발생한다!!
- "m3의 값은 m1, m2의 합이다" 라는 표시만 해두고, 필요할 때까지 덧셈을 지연시키자.
Matrix<int> m4(1000, 1000);
... // m4에 몇 개의 값을 준다.
m3 = m4 * m1; // m3는 덧셈해서 할당할 필요조차 없는 코드였다!
// m4 * m1 역시 플래그 처리만 해두고 계산을 지연시켜둔다.
- 계산이 필요한 순간이 오긴 온다.
- 가장 빈번한 시나리오는 일부분만 계산하는 것.
cout << m3[4]; // m3의 네 번째 행을 콘솔에 출력한다.
// m3의 네 번째 행은 계산되어야 한다.
// 나머지는 여전히 계산을 지연시킬 수 있다.
- 물론 모두 사용된다면, 더 이상 할인 혜택은 없다.
cout << m3; // 이제는 미룰 수 없다. 모두 계산되어야 한다.
- 또한, 예외 상황들을 잘 고려해야 한다.
m3 = m1 + m2;
m1 = m4; // 이제 m3는 (이전 m1) + m2 가 되어야 한다!!
- 위 아이디어는 APL이라는 언어에 그 뿌리를 두고 있다.
- 행렬 기반 계산을 대화식으로 할 수 있도록 개발된 1960년 언어
- 그 당시 컴퓨터 성능은 매우 구닥다리였는데, 아주 큰 행렬의 덧셈, 곱셈, 나눗셈까지도 바로 처리했다.
- 수치 계산에 있어서는 지연 평가 구현에 적지 않은 품이 들어간다는 점을 생각하고 트레이드 오프를 잘 계산해야 한다.
결론
- 모든 계산 경과를 즉시 사용해야 하는 경우가 잦다면 지연 평가는 그다지 유용하지 않다.
- 심지어 더 많은 자원을 사용하는 꼴이 된다.
- 프로파일링 중 병목현상을 발견하면, 즉시 평가에서 지연 평가로 수정하기 용이하다.
- 인터페이스에는 즉시 평가인지 지연 평가인지 표가 나지 않기 때문이다.
728x90
'개발 > More Effective C++' 카테고리의 다른 글
[More Effective C++] 19. 임시 객체(temporaries) (0) | 2024.08.26 |
---|---|
[More Effective C++] 18. 과도 선행 평가 (0) | 2024.08.23 |
[More Effective C++] 16. 파레토 법칙 (0) | 2024.08.21 |
[More Effective C++] 15. 예외 처리 비용 (0) | 2024.08.20 |
[More Effective C++] 14. 예외 지정(예외 명세) (0) | 2024.08.19 |
Comments