'컴퓨터 이야기'에 해당되는 글 95건

  1. 2010.05.25 TR1 간단하지만 긴 소개
  2. 2010.05.25 헤더 파일은 적당히 나눠야 한다
  3. 2010.05.25 전 처리기 pragma 키워드
  4. 2010.05.25 f()와 f(void)의 차이점과 활용
  5. 2010.05.25 Duff's device 1

TR1 간단하지만 긴 소개

컴퓨터 이야기/C++ 2010. 5. 25. 04:06

[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

(다음 글은 다음 원문의 후반부를 의미를 해치지 않는 선에서 약간 편집하면서 번역한 것입니다. 전반부를 번역한 글은 여기를 클릭하세요)

원문 보기: Dr. Dobb's | The Technical Report on C++ Library Extensions | 5/9/2005

TR1에 추가된 컨테이너

Alex Stepanov, Dave Musser, Meng Lee 세 사람에 의해 구현된 STL이 C++ 표준 라이브러리의 가장 혁신적인 부분입니다(자세한 건 Alexander Stepanov and Meng Lee, "The Standard Template Library," HP Technical Report HPL-95-11 (R.1), 1995 를 보세요). sortrandom_shuffle 과 같은 일반화된 STL 알고리즘들은 vector 와 deque 과 같은 STL 컨테이너에 저장된 객체, 기본 배열에 저장된 객체, 심지어는 나중에 정의될 만한 데이터 구조상의 객체에 적용할 수 있습니다. 왜냐하면 반복자(iterator)를 통해 그 객체틀에 접근할 수 있기만 하면 되니까요. 바꿔말하면, 새로 정의한 알고리즘이 반복자 인터페이스를 통해 객체에 접근하기만 하면 기정의된 STL 컨테이너뿐 아니라 앞으로 정의될 컨테이너에도 작동할 수 있게 됩니다. 게다가 함수 호출 형식으로 불러 쓸 수 있는 "함수 객체"를 통해 STL 알고리즘의 작동 방식을 바꿀 수 있도록 해주기도 합니다. 예를 들어, find_if-인자로 주어진 범위내에서 주어진 조건에 맞는 특정 요소를 찾는 알고리즘-는 조건을 "함수 객체"로 넘겨 받고, sort 는 비교 연산 자체를 "함수 객체"로 넘겨 받습니다.

STL 이 워낙 확장이 용이하도록 설계되었기 때문에 사람들이 얼마 지나지 않아 확장하기 시작했습니다. 컨테이너에 꼭 있었으면 하는 게 몇 가지 있었습니다. STL 은 개수 변경이 가능하고 순서화된 요소(variable-sized sequence of elements)를 위한 세 가지 컨테이너를 제공했습니다: vector, list, deque. 그리고, balanced tree에 기반한 집합 및 사전 클래스도 포함하고 있었죠: "소위 연관 컨테이너"인 set, map, multiset, multimap. 이걸 보고 나면 보통 한 가지—hash tables—가 빠졌다는 생각이 들기 마련입니다. Perl, Python, Java 와 같은 다른 언어들은 다 hash 기반 사전 클래스가 있는데, C++ 라고 없으란 법 있겠습니까 ?

이렇게 된 건 다 역사적인 것 때문이었습니다. STL에 hash table 을 추가하자고 1995년에 제안됐었습니다(자세한 건 Javier Barreiro, Robert Fraley, and David R. Musser, "Hash Tables for the Standard Template Library," X3J16/94-0218 and WG21/N0605, 1995 를 보세요). 그런데, 표준에 반영하기에는 너무 늦었던 것이죠. 각 벤더들은 나름대로 이 간극을 매꿔꿨습니다. 구현된 주요 라이브러리 대부분이 어떤 형태로든 hash table을 포함하고 있습니다. 예를 들자면, Microsoft Visual C++ 에 딸려오는 Dinkumware, GCC에 딸려오는 GNU libstdc++ 등이 있습니다. 비슷한 기능을 하는데 서로 호환되지 않는 구현들이 있다면 자연스럽게 표준화의 대상이 되기 마련이고, 결국 TR1에 이르러서 hash table이 표준에 포함된 것이죠.

TR1 hash table 은 특징적인 기능을 많이 가지고 있습니다만 간단한 예제에서는 표준 연관 컨테이너가 사용되는 것과 거의 차이점이 없습니다. 다음 코드를 보면 hash table 클래스 이름이 약간 이상하게 느껴질 수도 있을 것 같네요.

#include <tr1/unordered_map>
#include <iostream>
#include <string>

int main()
{
  using namespace std;
  using namespace std::tr1; 
  typedef unordered_map<string, unsigned long> Map;   // 여기
  Map colors;

  colors["black"] = 0xff0000ul;
  colors["red"] = 0xff0000ul;
  colors["green"] = 0x00ff00ul;
  colors["blue"] = 0x0000fful;
  colors["white"] = 0xfffffful;

  for (Map::iterator i = colors.begin();
       i != colors.end();
       ++i)
  cout << i->first << " -> " << i->second << endl;
}

다른 라이브러리에서는 보통 hash_map 이라고 이름을 지었는데, TR1에서는 왜 unordered_map 이라고 이름을 지었을까요 ? 안타깝게도, 이전 라이브러리에서 hash_map 이라는 이름을 이미 써버렸기 때문에 다른 이름을 쓸 수 밖에 없었던 거죠. 기존의 STL hash table 들이 서로 호환되질 않았기 때문에 TR1 hash table 이 기존 것들과 호환되게 만드는 게 불가능해서 기존과는 다른 인터페이스를 채용했는데, 그러다 보니 다른 이름을 쓰는 게 낫겠다는 결론에 도달하게 됐던 것입니다.

순서 없는 연관 컨테이너도 다른 STL 컨테이너와 마찬가지로 동일한(homogeneous) 요소들만 허용합니다. 다시 말하자면, std::vector<T> 안에 있는 요소던지std::trl::unordered_set<T> 에 있는 요소던지 다 같은 타입을 가져야 한다는 거죠. 표준에 다른 타입이 허용되는 컨테이너가 딱 하나있기는 합니다: std::pair<T, U> 는 서로 다른 타입일 수 있는 두 개의 객체를 포함하고 있습니다. pair 는 항상 두 객체를 하나로 묶어야할 경우에 유용합니다. 함수가 여러개의 값을 리턴해야할 때가 그런 경우입니다. 예를 들어, 연관 컨테이너인 set 과 map (unordered_set 과 unordered_map도 동일함)은 pair<iterator, bool> 를 리턴하는 insert 멤버 함수가 있습니다. 리턴값의 bool 부분은 새로운 요소가 진짜로 삽입이 됐는지 아니면 이전에 이미 같은 키를 가진 요소가 있었는지를 구분해줍니다. iterator 부분은 이전에 이미 존재하던 요소를 가리키거나 새로 삽입된 요소를 가리킵니다.

그런데, 꼭 두 개의 값을 묶을 때만 pair 를 쓸 수 있다는 건 너무 심한 제약이 아닐까요 ? 함수가 꼭 하나의 값만 리턴하라는 법이 없듯이 두개의 값만 리턴하라는 법도 없을테니까요. 수학에서 n-tuple 이라는 게 순서가 있는 n 개 값의 묶음이라는 건 기억하실 겁니다. pair는 n = 2 인 경우의 tuple 이라고 할 수 있을 겁니다. TR1은 이런 다른 타입이 가능한(heterogeneous) 컨테이너로 std::tr1::tuple 를 도입했습니다.

tuple 를 구현하는 건 상당히 고수준의 프로그래밍을 필요하지만, 사용하는 건 무척 간단합니다. pair 랑 비슷하게 템플릿 파라미터로 값들의 타입을 몇 개라도 제공하기만 하면 됩니다. (물론 파라미터 숫자에 한계가 있긴하지만 통상적으로 상당히 큰 값입니다) 예를 들어 다음과 같이 말입니다:

#include <tr1/tuple>
#include <string>

using namespace std;
using namespace std::tr1;
...
tuple<int, char, string> t = make_tuple(1, 'a', "xyz");


어떤 함수가 tuple 을 통해 여러 개의 값을 리턴한다면 pair 와 비슷하게 당연히 값을 한번에 하나씩 꺼내올 수 있기를 바랄 것입니다. 그럴 경우 다음과 같이 하시면 됩니다:

tuple<int, int, int> tmp = foo();
int x = get<0>(tmp);
int y = get<1>(tmp);
int z = get<2>(tmp);

pair first 와 second  멤버 변수를 통해 엑세스하는 것과는 약간 다르지만 아이디어는 기본적으로 비슷합니다. tuple 을 이용하면 약간 더 쉽게 할 수도 있습니다. 임시 변수를 사용하는 대신 그냥 다음과 같이 하시면 됩니다:

tie(x, y, z) = foo();

그러면, 복잡한 일은 라이브러리가 다 알아서 해주게 됩니다.

보통 함수가 여러개의 값을 리턴해야할 때 쓰는 방법이 여러개의 참조자 매개변수를 쓰거나, 리턴 타입으로 쓸 ad hoc 클래스를 정의하는 것입니다(div_t을 생각해 보세요). tuple 을 활용한다면 그럴 필요가 없어지겠죠.

Infrastructure: Smart Pointer 와 Wrapper

tuple 이 유용한 이유는 string 이 유용한 이유와 비슷합니다. 상당히 기본적인 기능이기 때문에 상위 수준의 라이브러리에서 인터페이스를 기술할 때 쓸 수 있다는 것입니다. 이런 기초적인 기능이 공통의 vocabulary로 사용돼서 독립적으로 작성된 부분들이 서로 communicate 할 수 있게 되는 것입니다. TR1은 이런 특성을 갖는 몇 가지 유틸리티를 추가했습니다.

자원의 라이프타임을 관리하는 일은 대부분 프로그램에서 발생하는 공통적인 문제입니다. 메모리를 할당하거나 네트워크 소켓을 열었다면, 언제 메모리를 해제하고, 언제 소켓을 닫아야 할까요 ? 이 문제에 대한 두 가지 해결책은 자동 변수를 사용하거나("resource acquisition is initialization," 또는 RAII) 또는 garbage collection을 이용하는 것입니다. 두 가지 방법이 유용하긴 하지만 모든 경우에 적용 가능한 건 아닙니다. RAII 는 자원의 라이프타임이 정적으로 정해질 수 있고, 프로그램의 lexical 구조에 맞아 떨어질 때-예를 들어, 한 함수의 scope 에서만 필요하다던지-, 쓸 수 있습니다. 반면 garbage collection 은 메모리 관리에는 적합하지만 다른 자원에는 필요 이상으로 복잡한 해결책일 것입니다. 이에 대한 다른 대안으로 참조 카운트 smart pointer를 들 수 있습니다.

참조 카운트 포인터의 기본 아이디어는 명확합니다. T* 타입의 raw 포인터를 사용하지 않고, 포인터 "처럼 보이는" wrapper 클래스를 사용하는 것입니다. 마치 보통 포인터처럼 '*' 연산자를 써서 포인터가 가리키는 객체에 접근하기 위해 dereference 할 수도 있고, '->' 연산자를 써서 객체 멤버에 접근할 수도 있습니다. 차이점이 있다면 이 wrapper 클래스는 몇 가지 기본 연산과 생성자, 소멸자, 할당 연산자를 나름대로 정의해서 특정 객체에 대해 얼마나 많은 소유자가 있는지에 대한 정보를 유지할 수 있다는 것입니다.

TR1 의 참조 카운트 포인터 클래스는 shared_ptr 입니다. 간단한 경우에 대해서는 auto_ptr  클래스처럼 작동합니다. new 연산자를 이용해 생성한 객체로 shared_ptr 를 초기화할 수 있고, 그 shared_ptr 를 가리키는 다른 shared_ptr 이 없다면 shared_ptr 이 현재 scope를 떠날 때 객체도 함께 소멸됩니다.

같은 객체를 가리키는 인스턴스가 여러개 있을 때 재밌는 일이 벌어지게 됩니다. 객체를 가리키는 마지막 shared_ptr  인스턴스가 사라질 때까지 객체는 소멸되지 않습니다. 생성자와 소멸자 호출을 로깅하는 클래스를 써서 이런식으로 작동하는 것을 쉽게 확인해 볼 수 있습니다. 다음 코드에서 보듯이 두 포인터 p1p2 는 모두 A 객체를 가리킵니다. 두 포인터 모두 현재 scope를 떠날 때 소멸될 것입니다. 그렇지만, shared_ptr  의 소멸자가  객체에 대한 소유자의 개수를 정확히 유지하고 있기 때문에 마지막  shared_ptr  이 소멸될 때에야 진짜 객체를 소멸시키게 됩니다.

#include <iostream>
#include <tr1/memory>

using namespace std;
using namespace std::tr1;

struct A {
  A() { cout << "Create" << endl; }
  A(const A&) { cout << "Copy" << endl; }
  ~A() { cout << "Destroy" << endl; } 
};

int main() {
  shared_ptr<A> p1(new A);
  shared_ptr<A> p2 = p1;
  assert (p1 != NULL && p2 != NULL && p1 == p2);
}

물론, 실제 예들은 이것 보다는 복잡할 것입니다. 전역 변수에 shared_ptr 을 할당할 수도 있으며, 함수 인자로 넘겨 주고, 리턴값으로 받을 수도 있으며, STL 컨테이너에 넣을 수도 있습니다. vector<shared_ptr<my_class> > 는 다형성 객체를 담을 수 있는 컨테이터를 구현할 수 있는 가장 손쉬운 방법입니다(자세한 것은 "Containers of Pointers," C/C++ Users Journal, 2001년 10월을 보세요).

TR1에서 shared_ptr 을 제공함으로써 다음 두 가지 이점을 얻을 수 있습니다.

  • tuple 및 string 과 마찬가지로 프로그램을 작성하기 위한 공통의 vocabulary 가 됩니다. 동적으로 할당된 메모리에 대한 포인터를 리턴하는 함수를 작성하고 싶다면 shared_ptr 을 리턴 타입으로 갖는 함수를 작성하면, 그 함수의 사용자도 shared_ptr 을 쓸 수 있다고 확신할 수 있게 되는 거죠. 나름대로 작성한 참조 카운트 클래스를 사용한다면 이렇게 확신할 수는 없을 것입니다.
  • 그리고, shared_ptr 은 잘 작동합니다. shared_ptr 은 겉보기와 다르게 그리 단순하지는 않습니다. 많은 사람들이 참조 카운트 smart pointer 를 작성하곤 했지만, 모든 corner case 를 제대로 처리하는 걸 만든 사람은 거의 없었습니다. 특히나 멀티쓰레딩 환경에서는 shared_ptr 같은 클래스는 잘못되기 쉽상입니다. 그러니 shared_ptr  같이 잘 테스트되고 사용된 경험도 풍부한 구현이 유용할 것입니다.

그렇다고 해서 참조 카운트 포인터가 모든 자원 관리 버그 문제를 해결해 주는 건 아닙니다. 여전히 어떤 프로그래밍 원칙이 필요합니다. 여전히 두 가지 문제점이 발생할 여지가 있습니다. 첫째로, 두 개의 객체가 서로를 가리킨다고 가정해 봅시다. If xy 를 가리키는  shared_ptr 을 포함하고 있고, yx 를 가리키는 shared_ptr 를 포함하고 있다면, 프로그램 내에서 x 또는 y 를 가리키고 소유자가 하나도 없더라도 어느 쪽의 참조 카운트도 0 이 되지 않게 됩니다. 이런 경우 cycle 이 형성되어 결국 메모리 누수가 일어나게 됩니다. 둘째로, 같은 객체에 대해 shared_ptr 과 일반 포인터를 함께 쓰는 경우입니다:

my_class* p1 = new my_class;
shared_ptr<my_class> p2(p1)
...

p1 이 마지막 shared_ptr (p2 이던 p2 의 복사본이던)보다 더 오래 존재하게 된다면 p1 은 유효하지 않은 포인터가 되어버립니다. 즉, 이미 소멸된 객체를 가리키게 된다는 것이죠. 이런 상황에서 p1 을 역참조하면 프로그램이 바로 비정상 종료하게 될 것입니다. 어떤 smart pointer 라이브러리는 smart pointer 내부의 raw 포인터를 접근하지 못하게 하여 이 문제를 해결하려고 시도하기도 하지만 shared_ptr 은 그렇지 않습니다. 일반 용도(general purpose)로 쓰는 smart pointer 클래스라면 당연히 내부의 raw 포인터를 보여줘야만 한다고 생각합니다. 그렇지 않으면 '->' 연산자가 작동할 수 없으니까요. 게다가 일부러 문법적인 장애를 두는 것은 정상적인 사용도 어렵게 만들뿐이니까요.

보통 사이클을 피하기 위해 참조 카운트 포인터와 raw 포인터를 섞어 쓰기 때문에 이 두 가지 형태의 잠재적인 버그 소스는 함께 따라 가는 경우가 많습니다. 두 개의 객체가 서로를 참조해야 하는 경우 사용할 수 있는 테크닉은 하나는 소유개념이 있는 포인터를 쓰고, 다른 하나는 소유개념이 없는 포인터를 쓰는 것입니다. shared_ptr 소유개념이 있는 포인터로 사용하고, 다른 포인터를 소유개념이 없는 걸로 사용하면 됩니다. 이런 테크닉은 소유개념이 없는 포인터로 raw 포인터를 사용하지 않는다면 완벽한 해결책이라고 할 수 있습니다. TR1 smart pointer 라이브러리는 이런 경우를 위한 더 나은 해결책-weak_ptr -을 제공합니다. weak_ptr 은 이미 shared_ptr  이 관리하고 있는 객체를 가리킬 수 있습니다. 마지막 shared_ptr  이 사라지더라도 객체가 소멸되는 것을 막아주지는 않지만, raw 포인터와는 달리 dangling reference가 되더라도 프로그램 비정상 종료를 일으키지 않고, 또한 다른 shared_ptr 과 소유권을 공유하는 shared_ptr 로 변환할 수도 있습니다. 다음 코드에서 shared_ptrweak_ptr 을 섞어 쓰는 예를 확인할 수 있습니다.

class my_node {
public:
  ...
private:
  weak_ptr<my_node> parent;
  shared_ptr<my_node> left_child;
  shared_ptr<my_node> right_child;
};

TR1 에는 smart pointer 말고도 몇 가지 다른 기본적인 기능을 포함하고 있습니다. 말하자면, 함수나 함수 객체 같은 것을 쉽게 해줄 수 있는 것들 말입니다. 그 중에 가장 유용한 게 function wapper 클래스 일 것 같습니다.

A 타입과 B 타입의 인자를 취하고 C 타입을 리턴하는 뭔가를 작성해야 한다고 칩시다. C++는  이런 걸 표현할 수 있는 다양한 방법을 제공합니다. 일반 함수로 작성할 수도 있고,

C f(A a, B b) { ... }

A 를 별도 클래스로 작성했다면 다음과 같이 멤버 함수로 작성할 수도 있을 것입니다.

class A {
...
C g(B b);
};

아니면 std::plus<T> 처럼 함수 객체로 작성할 수도 있습니다.

struct h {
C operator()(A, B) const {...}
};

물론 이런 방법들은 서로 문법에서나 의미론에서나 차이가 있습니다. 특히, 멤버 함수의 경우 다른 방식으로 호출이 됩니다. mem_fun  을 이용하면 문법적 차이는 극복할 수 있습니다.(또는 TR1의 mem_fn 이나 bind 를 이용하면 더 편하게 할 수 있습니다). 근데 피할 수 없는 게 한 가지 있습니다. 바로 type 입니다. A×B->C 라는 같은 처리를 하는 세 가지 다른 방법이 있는데, 언어 입장에서 보면 이 세 가지 방법은 모두 다른 타입입니다. 이런 다른 방법을 하나의 타입으로 표현할 수 있다면 유용할 것입니다.

그게 바로 TR1의 function wrapper 클래스가 하는 일입니다. function<C(A,B)> 이라는 타입은 A 와 B 를 취하고, C 를 리턴하는 어떤 함수라도 표현할 수 있습니다. 그게 일반 함수이던, 멤버 함수이던, 함수 객체이던 상관 없이 말입니다. function 를 구현하는 건 상당히 복잡하지만 그런 복잡함은 라이브러리에 모두 감춰져 있으니 사용자 입장에서는 신경 쓰지 않아도 됩니다. 그저 적당한 타입을 갖는 function 을 인스턴스화하고 의미가 있는 어떤 거라도 할당한 후, 일반 함수 호출하듯 쓰면 되는 것입니다(물론, 인자 개수에 제한이 있긴 하지만, 제한도 보통은 상당히 클 겁니다)

예를 들어, my_class 를 인자로 취하고 아무것도 리턴하지 않는 어떤 함수라도 function<void(my_class)> 를 통해 호출할 수 있습니다. 다음 코드를 참고하시기 바랍니다.

#include <tr1/functional>
#include <vector>
#include <iostream>

using namespace std;
using namespace std::tr1;

struct my_class
{
  void f() { cout << "my_class::f()" << endl; }
};

void g(my_class) {
  cout << "g(my_class)" << endl;
}

struct h {
  void operator()(my_class) const {
    cout << "h::operator()(my_class)" << endl;
  }
};

int main()
{
  typedef function<void(my_class)> F;
  vector<F> ops;
  ops.push_back(&my_class::f);
  ops.push_back(&g);
  ops.push_back(h());

  my_class tmp;
  for (vector<F>::iterator i = ops.begin();
       i != ops.end();
       ++i)
    (*i)(tmp);
}

function<void(my_class)> 객체를 vector 에 넣은 것은 function wrapper 클래스가 왜 유용한지를 보여주기 위해 일부러 그런 것입니다. 한 마디로 표현하자면, callback 입니다. 상위 라이브러리가 사용자가 넘겨준 callback 을 담고 있을 일관된 메커니즘을 갖게 된 것입니다. 이러한 메커니즘이 나중에 dynamism 과 loose coupling을 필요로 하는 새로운 라이브러리에 널리 사용될 거라고 예상됩니다.

Application 용: 정규식

다른 라이브러리 작성을 도와주는 저수준 컴포넌트도 중요하겠지만, 프로그래머의 문제를 바로 해결해 줄 수 있는 라이브러리 컴포넌트도 중요할 것입니다. 대부분의 프로그램은 텍스트를 처리해야 하는데, 텍스트 처리에 사용되는 고전적인 방법중에 하나가 정규식을 이용한 패턴 매칭입니다. 정규식은 컴파일러, 워드 프로세서, 설정 파일을 읽어야 하는 대부분의 프로그램에 이용되고 있습니다. Perl 에서는 정규식을 상당히 잘 지원하기 때문에 텍스트를 처리하는 스크립트를 작성하기가 무척 쉽습니다. 반대로 C++ 에서는 정규식을 잘 지원하지 않아서 C++ 의 약점이 되곤했죠. 다행이 TR1에서는 정규식을 지원하기 시작했습니다.

TR1 정규식은 상당히 풍부한 기능과 선택사항을 제공하지만 기본적 사용법은 아주 간단합니다. Python 이나 자바에서 정규식을 사용해 봤다면 친숙하게 느껴지실 겁니다. 먼저 매치하고 싶은 패턴-ECMA-262 의 표준 문법에 따라 표현해야 합니다. JavaScript 에서도 같은 문법을 사용하고 있습니다-을 표현하는 tr1::regex 객체를 생성한 후, 문자열에 대해 그 패턴을 매치하는 알고리즘 (regex_match, regex_search, regex_replace) 중 하나를 수행하면 됩니다. regex_match 는 문자열이 정규식 패턴에 의해 매치되는지 여부를 검사하는 반면, regex_search 는 문자열이 패턴에 매치되는 substring을 포함하고 있는지를 검사합니다.  regex_matchregex_search 공히 매치가 성공했는지를 나타내기 위해 bool 을 리턴합니다. 또한 match_results 를 사용해서 더 자세한 매치 결과를 알아낼 수도 있습니다.

TR1 정규식 라이브러리는 표준 IOStream 라이브러리 같이 템플릿 인자를 지원하긴 하지만 대부분 경우 템플릿 인자를 사용하지 않게 될 것입니다. 아마 basic_regex 보다는 regex-basic_regex<char> 의 줄임 표현입니다-를 사용하게 될 것이고, 이건 basic_istream 보다는 istream 을 사용하게 되는 현상과 비슷한니다. match_results 도 두 가지 specialization 이 있어서 match_results 를 직접 쓰진 않게 될 것 같습니다. 문자열(string)을 검색한다면 smatch 를 사용하면 되고, 문자 배열(array of char)을 검색한다면 cmatch 를 사용하면 됩니다.

다음 코드는 UNIX grep 핵심 부분을 작성하는 데 TR1 정규식을 사용하는 방법을 보여 주고 있습니다.

#include <regex>
#include <string>
#include <iostream>

using namespace std;
using namespace std::tr1;

bool
do_grep(const string& exp, istream& in, ostream& out)
{
  regex r(exp);
  bool found_any = false;
  string line;

  while (getline(in, line))
    if (regex_search(line, r)) {
    found_any = true;
    out << line;
  }

  return found_any;
}

do_grep
은 매치하는지 여부만 다루고 있고, substring 은 다루지 않고 있습니다. 정규식을 사용하는 주요 이유 중 하나가 복잡한 문자열을 개별 필드로 변환하기 위해서입니다. 다음 (가), (나) 코드는 미국식 또는 유럽식 날짜 표현을 서로 변환하기 위한 예제를 통해서 이런 경우에 대한 예를 든 것입니다.

(가)

const string datestring = "10/31/2004";
const regex r("(\\d+)/(\\d+)/(\\d+)");
smatch fields;

if (!regex_match(datestring, fields, r))
  throw runtime_error("not a valid date");

const string month = fields[1];
const string day = fields[2];
const string year = fields[3];

(나)

const string date = "10/31/2004";
const regex r("(\\d+)/(\\d+)/(\\d+)");
const string date2 = regex_replace(date, r, "$2/$1/$3");

regex_search 는 한 문자열 안에 하나 이상의 매치가 있다고 하더라도 항상 첫번째 매치만을 리턴합니다. 모든 매치를 다 찾고 싶다면 어떻게 해야할까요 ? 다음 코드와 같이 정규식 반복자를 사용해서 분해한 후에 vector 를 초기화하면 됩니다.

const string str =
"a few words on regular expressions";
const regex pat("[a-zA-Z]+");

sregex_token_iterator first(str.begin(),
str.end(), pat);
sregex_token_iterator last;

vector<string> words(first, last);

미래:

확실히 TR1 전체를 한 기사로 다루는 건 무리가 있네요. shared_ptrfunction 은 비교적 자세히 언급했지만, reference_wrapper, result_of, mem_fn, bind 는 중간에 잠깐 스치는 식으로만 언급했습니다. 그리고, tuple 과 순서 없는 연관 컨테이너(unordered_set, unordered_map)은 자세히 언급했지만 array<T, N> 에 대해서는 전혀 언급하지 못했습니다. 라이브러리 작성자들에게 유용한 타입 특성자(type traits), 난수 생성기, 특수한 수학 함수 등에 대해서는 다루지 못했습니다. Bessel 함수, hypergeometric 함수, Riemann z 함수 등에 관심이 있다면 한 번 직접 찾아보시길 바랍니다. 마지막으로 C99 호환성에 대해 다루진 않았지만, 예상하는 대로 잘 작동할 것입니다.

이 글을 쓰는 현재(2005년 5월) TR1 라이브러리를 완벽하게 구현한 제품은 없지만 작업 중인 것이 몇 가지 있긴 합니다:

  • Metrowerks CodeWarrior 9.0 은 TR1 을 부분적으로 구현하고 있습니다. function, shared_ptr, tuple 등이 지원됩니다.
  • smart pointer, 정규식, 난수 생성기 등 TR1의 많은 부분들이 Boost 에서 넘어왔습니다. Boost 는 상당히 많은 컴파일러와 플랫폼에서 무료로 이용 가능합니다.
  • Dinkumware 는 현재 TR1 전체를 구현 중에 있습니다. 현존하는 가장 좋은 sincos 구현에 필적할만한 정확도를 달성할 목표를 가지고 cyl_bessel_jriemann_zeta 를 구현 중에 있는 유일한 회사입니다.
  • GNU libstdc++ 는 TR1 을 열심히 구현 중에 있고, GCC 다음 릴리즈인 GCC 4.0에 TR1이 부분적으로 제공될 예정입니다. 어느 정도나 제공될지는 장담하기는 어렵습니다.

TR1 은 허풍으로 말하는 라이브러리가 아니라 진짜 쓸 수 있는 라이브러리입니다. 이 기사에 제시된 모든 코드는 실제 컴파일 해 본 것들입니다(물론, "..." 부분만 빼구요 ^^;). GNU libstdc++ 가 아직 실험적이고 완벽하진 않지만, 순서 없는 연관 컨테이너, tuple, functional, smart pointers 등은 테스트할 수 있었습니다. TR1 정규식은 libstdc++ 에서 아직 제대로 구현하지 않고 있기 때문에 대신 Boost.Regex를 사용했습니다. 헤더 파일과 네임스페이스만 바꿔서 말입니다.

표준화 위원회가 TR1 작업을 거의 마쳤기 때문에 다음 단계의 라이브러리 확장에 대해 생각할 때가 됐습니다. 다음에는 뭘 기대할 수 있을까요 ? 표준화 위원회가 아직 TR2 제안서에 대한 얘기도 시작하지 않아서 아직 말하기에는 너무 이른 감이 있습니다. 저는 사람들이 요청하는 확장 기능을 유지하고 있습니다(http://docs.google.com/View?docid=ajfb44js8vjx_bchdmtqvpnxv4 원문에는 http://lafstern.org/matt/wishlist.html 최근들어 writely 로 바꾸었나 봅니다). 물론, wish 리스트가 진짜 제안서로 나오기까지는 긴 여정이 필요하겠지요.

제가 진짜 원하는 건 실제적인 일-HTML 과 XML 파싱하기, GIF 와 JPEG 이미지 처리하기, 디렉토리 읽기, HTTP 처리하기 등-을 처리하는 라이브러리입니다. 문자열 공백문자 없애기, 문자열을 대문자로 바꾸기와 같은 작업도 간단히 할 수 있었으면 좋겠습니다. 라이브러리 작성자가 사용할 수 있는 일반적인 infrastructure 를 만들어 두었으니, 그걸 이용해서 매일 매일 사용할 수 있는 라이브러리를 만들 때가 아닐까요 ?

그렇지만 표준화 위원회는 표준화 작업을 수행하지 새로운 것을 만들어내지는 않는다는 것을 기억할 필요가 있습니다. 실제 필요한 일을 수행한다고 생각되는 라이브러리들만 TR2에 들어가게 될 것입니다. 여러분은 전체 "wish 리스트" 나 저의 제안이나, Boost 라이브러리에서 영감을 얻을 수도 있을 것입니다. 몇 년 전에 쓴 기사에서 밝인 바와 같이(자세한 것은 "And Now for Something Completely Different," C/C++ Users Journal, 2002년 1월을 보세요), 라이브러리 확장 제안서는 특정 문제점 영역이 왜 중요한지, 해결책은 어떤 식인지, 그 해결책이 기존의 것과 어떻게 연관되어 있는지, 라이브러리 전체에 어떤 영향을 미칠 것인지에 대해 설명해야 합니다. 그리 간단한 일은 아니지만 이제 그 때 당시 보다는 수월해졌습니다. 이제 라이브러리 확장 제안서를 어떤 식으로 작성해야 하는지 샘플이 있기 때문입니다. TR1 당시 수락된 라이브러리 확장 제안서 모음이 http://open-std.org/jtc1/sc22/wg21/docs/ library_technical_report.html 에 있으며, TR2 제안서의 모델로 활용될 예정입니다.

TR1이 완성되었으니 다음 단계를 생각할 때입니다. 다음 단계는 여러분에게 달려 있기도 합니다!

참고문헌

Austern, Matthew H. Generic Programming and the STL: Using and Extending the C++ Standard Template Library, Addison-Wesley, 1998.

Josuttis, Nicolai. The C++ Standard Library: A Tutorial and Reference, Addison-Wesley, 1999.

Langer, Angelika and Klaus Kreft. Standard C++ IOStreams and Locales: Advanced Programmer's Guide and Reference, Addison-Wesley, 2000.

Lischner, Ray. STL Pocket Reference, O'Reilly & Associates, 2003.

Meyers, Scott. Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library, Addison-Wesley, 2001.

Musser, David R., Gilmer Derge, and Atul Saini. STL Tutorial and Reference Guide: C++ Programming with the Standard Template Library, Second Edition, Addison-Wesley, 2001.

Plauger, P.J., Alexander A. Stepanov, Meng Lee, and David R. Musser. The C++ Standard Template Library, Prentice Hall, 2000.

(번역하는 게 정말 쉽지 않네요. 전반부 번역한 후 빨리 후반부도 번역하려고 했는데, 결국 일주일이나 걸렸습니다. 매일 매일 짬짬히 번역했는데도 상당히 걸리네요. 그래도 번역이 나름 잘된 것 같아(제 기준으로) 마음은 뿌듯합니다.혹시 원문이랑 비교해 보시고 좀 이상하다 싶으면 좀 알려 주세요)

http://yesarang.tistory.com/56 출처

'컴퓨터 이야기 > C++' 카테고리의 다른 글

파일 다이얼로그  (0) 2010.07.17
Self documenting 합시다!!  (1) 2010.07.17
헤더 파일은 적당히 나눠야 한다  (0) 2010.05.25
전 처리기 pragma 키워드  (0) 2010.05.25
f()와 f(void)의 차이점과 활용  (0) 2010.05.25
:

헤더 파일은 적당히 나눠야 한다

컴퓨터 이야기/C++ 2010. 5. 25. 04:02

참 오랜만에 글을 올려 보네요. ^^; 이번 글도 오래전부터 생각은 하고 있었는데, 이제서야 맘을 먹고 쓰게 됐습니다. 언제나 이 게으름을 극복할 수 있을까요 ?

이번 글에서는 헤더 파일과 컴파일 시간간 관계에 대해서 얘기하려고 합니다. 실제 실험을 통해 설명하는 게 쉬울 것 같습니다.

C++에는 Standard Template Library 라는 게 있다는 건 모두 아시리라 생각합니다. STL의 헤더 파일을 보면 <vector>, <list>, <map>, <deque>, <stack> 등 각 필요한 기능별로 헤더 파일이 비교적 상세하게 나눠져 있는 걸 볼 수 있습니다. 만약 STL의 개발자가 이렇게 상세하게 나눠놓지 않고 <stl>이라는 하나의 헤더 파일만 include 하면 되도록 개발해 놓았다면 컴파일 시간에 어떤 영향을 미치게 될까요 ? 실험을 통해 어떤 영향이 있을지 살펴 보겠습니다.

다음 두 개의 소스를 비교해 보시기 바랍니다.

/// @file main.cpp
/// 지금처럼 STL 헤더 파일이 비교적 상세하게 나눠져 있는 경우
#include <vector>   /// 필요한 헤더 파일만 include 합니다.

using namespace std;

int main(void)
{
  vector<int> vi;

  return 0;
}

/// @file main_all.cpp
/// STL header 파일이 stl 하나로 합쳐져 있는 경우
#include <stl>    /// 전체 라이브러리 헤더 파일을 include 합니다.

using namespace std;

int main(void)
{
  vector<int> vi;

  return 0;
}

/// @file stl
/// STL 헤더 파일이 stl 하나로 합쳐진 경우를 흉내냄
#include <vector>
#include <list>
#include <map>
#include <set>
#include <deque>
#include <stack>
#include <functional>
#include <algorithm>

이 둘 간의 100번씩 연속해서 컴파일한 후 시간을 비교해 봤더니 다음과 같은 결과가 나오더군요.

main

real    0m23.919s
user    0m19.309s
sys     0m2.488s

main_all

real    0m32.496s
user    0m27.194s
sys     0m2.900s

참고로 제가 테스트한 환경은 다음과 같습니다.

Ubuntu 8.04
g++ 4.2.3
$ cat /proc/cpuinfo
processor    : 0
vendor_id    : GenuineIntel
cpu family   : 6
model        : 13
model name   : Intel(R) Pentium(R) M processor 2.00GHz
stepping     : 8
cpu MHz      : 798.000
cache size   : 2048 KB

위 결과에서 두 경우가 24:32로 상당한 컴파일 시간 차이가 있음을 알 수 있습니다. 헤더 파일을 하나로 합쳤을 때가 그렇지 않을 경우에 비해 거의 25% 정도 늘어나는 것을 확인할 수 있습니다.

제가 이전에 사내 어떤 공용 라이브러리 소스에서 모든 헤더 파일을 하나의 헤더 파일에서 include 해 놓고, 각 응용 프로그램은 그 헤더 파일만 include 하도록 해 놓은 것을 본 적이 있습니다. 이렇게 할 경우 위 실험 결과를 생각해 보건데, 전체 응용 프로그램을 컴파일하는데 얼마나 시간이 많이 걸릴지 예상할 수 있습니다.

많은 개발자들이 컴파일 시간에 대해서는 별로 신경쓰지 않지만, 프로젝트 규모가 커지다 보면 컴파일 시간이 기다리기 힘들 정도로 길어지는 경우가 많습니다. 그렇게 될 경우, 코딩-컴파일-디버깅 싸이클이 길어지게 되고, 전체 개발 일정에도 영향을 미칠 수 있습니다.

요즘은 빌드 기술도 많이 발전하여 distributed build니 parallel build니 컴파일 시간을 많이 단축시켜 주는 툴이 있기도 합니다. 프로젝트 규모가 워낙 크고 어느 정도 개발이 진행되었다면 이런 툴들을 활용하는 것이 당연하겠지만, 그보다는 먼저 개발하기 전에 소스 파일들 간의 dependency 가 너무 많이 걸려 있지는 않도록 주의깊게 설계하는 것도 중요합니다. 그런 dependency 를 줄일 수 있는 방법 중의 하나가 이 글에서 제안하고 있는 헤더 파일을 적당히 나누라는 규칙이 되겠습니다. 다음 경우를 생각해 보시기 바랍니다.

all.h가 a.h, b.h ~ z.h 까지 포함하고 있고, a.c 라는 소스는 b.h에 있는 함수를 사용하지만, all.h만을 include하도록 해 놓았다면 어떻게 될까요 ? a.c 의 dependency list 에는 all.h 뿐 아니라 a.h ~ z.h 도 모두 포함됩니다. 그렇다면, 그 중에 하나만 수정되더라도 a.c 는 재컴파일 될 것입니다. 더불어 all.h 를 include 했던 모든 소스 파일들이 컴파일 되겠지요.

물론 헤더 파일을 아주 상세하게 나눈다면, 심지어 함수 하나에 헤더 파일 하나 정도로 나눌 수도 있겠지요. 그렇지만 이렇게 할 경우, 너무 많은 헤더 파일들을 열어야 하니, 역시 개발자들이 불편하게 될 것입니다. 너무 많은 헤더 파일을 열게 되니, 역시 컴파일 시간에 좋지 않은 영향을 미칠 수도 있을 것이구요. 그렇다면 적당히 중용을 찾아야 할 것입니다.

프로젝트 규모가 커질 수록

헤더 파일은 적당히 나눠야 한다

라는 규칙을 명심하시기 바랍니다. 적당을 위한 기준은 개발편의성과 컴파일시간간의 tradeoff 임도 기억하시기 바랍니다.

참고로 실험을 위해 다음과 같이 Makefile 을 작성했습니다.

CXX = g++
CFLAGS = -Wall -c -I. -o
LDFLAGS = -lstdc++
SRCS = main.cpp main_all.cpp

main: main.o

main_all: main_all.o

%.o:%.cpp
        $(CXX) $(CFLAGS) $@ $<

depend:
        makedepend -- $(CFLAGS) -- $(SRCS)

clean:
        rm -f *.o main main_all core* *.bak

make 를 호출하기 위한 shell script 를 다음과 같이 작성하였고,

$ cat incltst
#!/bin/bash

usage()
{
  echo "Usage: incltst main|main_all"
}

if [[ $1 != "main" && $1 != "main_all" ]]
then
  usage
  exit 0
fi

for (( i=0 ; $i < 100 ; ++i )) ; do
  make $1
  make clean
done

실행 시간은 다음 명령을 수행하여 측정하였습니다.

$ time ./incltst main
$ time ./incltst main_all

다른 분들도 측정하실 수 있도록 소스 파일 첨부합니다.


여러분이 실험한 결과도 한 번 댓글로 알려 주시면 감사하겠습니다.

글 읽어 주셔서 감사합니다. 이 글의 출처를 다음과 같은 형식으로 알려주신다면 비상업적 용도로 얼마든지 퍼다 나르셔도 좋습니다. ^^;

원문출처: 헤더 파일은 적당히 나눠야 한다(김윤수의 이상계를 꿈꾸며)

'컴퓨터 이야기 > C++' 카테고리의 다른 글

파일 다이얼로그  (0) 2010.07.17
Self documenting 합시다!!  (1) 2010.07.17
TR1 간단하지만 긴 소개  (0) 2010.05.25
전 처리기 pragma 키워드  (0) 2010.05.25
f()와 f(void)의 차이점과 활용  (0) 2010.05.25
:

전 처리기 pragma 키워드

컴퓨터 이야기/C++ 2010. 5. 25. 03:48

이 팁은 담비님(천리안 FREKBS)님이 98년 12월 14일에 천리안 프로그래머포럼에 올리신 것입니다. 

-------------------------------------------------------------------------------- 
안녕하세요! 담비입니다. 

본 문서는 제가 가지고 있는 책과 제공된 도움말을 기초로 하여 작성되어졌습니다. 
제가 영어 실력이 부족한 탓으로 내용의 이해가 약간(?)은 힘들거나, 문맥상의 
오류가 있을 수 있습니다. 혹 이런 내용을 발견하시거나, 잘못된 내용을 
발견하시거나, 추가되어져야 할 내용을 발견하신다면 메일 주시면 감사하겠습니다. 
보내주신 내용은 검토 후 문서에 재반영하여 올리도록 하겠습니다. 

frekbs@chollian.net 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
선행처리기중의 하나인 pragma에 관한 사용법을 정리하여 올립니다. 
문법은 다음과 같습니다. 


┏━━━━━━━━━━━┓ 
┃#pragma directive-name┃ 
┗━━━━━━━━━━━┛ 

#pragma는 이것을 지원하는 다른 compiler에서 방해가 없이 C++ Builder에서 원하는 
지시어를 정의할 수 있도록 해줍니다. 만일 지시명을 인식하지 못한다면 에러 또는 
경고 메세지를 수반하지 않고서 #pragma의 지시를 무시하게 됩니다. 

Borland C++ Builder에서 지원하는 #pragma지시어는 모두 18가지가 있습니다. 
이제부터 그것들을 하나 하나 살펴보기로 하겠습니다. 

1. #pragma anon_struct 
   . 사용법 
     #pragma anon_struct on 
     #pragma anon_struct off 
   . Class에 익명의 구조체를 포함하여 compile하는것을 허락할 것인지를 
     지시합니다. 익명이란 tag를 갖지 않는다는것을 의미합니다. 
   ex) 
   #pragma anon_struct on 
   struct S { 
      int i; 
      struct {   
         int   j ; 
         float x ; 
      }; 
      class {   
      public: 
         long double ld; 
      }; 
   S() { i = 1; j = 2; x = 3.3; ld = 12345.5;} 
   }; 
   #pragma anon_struct off 

   void main() { 
      S mystruct; 
      mystruct.x = 1.2;   
  } 

2. #pragma argsused 
   . argsused 프라그마는 함수 정의 사이에서만 허용되고 바로 다음 함수에만   
     영향을 미치며 경고 메세지를 disable시킵니다. 
     이 pragma를 사용하지 않은 경우 사용되지 않은 argument가 있으면 
     "Parameter name is never used in function func-name" 
     라는 경고 메세지를 표시하게 됩니다. 
   ex) 
   #pragma argsused 
   void __fastcall TImageForm::FileEditKeyPress(TObject* Sender, Char &Key) 
   {     if (Key == 0x13) { 
            FileListBox1->ApplyFilePath(FileEdit->Text); 
            Key = 0x0; 
         } 
   } 
   위의 예에서는 함수내에서 Sender라는 인수가 사용되지 않았지만 경고 메세지가 
   표시되지 않습니다. 

3. #pragma codeseg 
   . 사용법 
     #pragma codeseg  <"seg_class">   
   . codeseg 프라그마는 함수들을 위치시킬 group, class 또는 segment의 이름을 
     줄수 있도록 지시합니다. 만일 option없이 사용하였다면 함수의 배치를 위해서 
     default code segment가 사용되어질것입니다. 결국 이 pragma를 사용하지 않는 
     경우와 동일한 결과를 가져옵니다. 


4. #pragma comment 
   . 사용법 
     #pragma comment (comment type, "string") 
   . comment 프라그마는 출력되어지는 file에 주석을 기록시킬것을 지시합니다. 
     comment type에 올수 있는 값들은 다음중의 하나가 될것입니다. 
     * exestr 
       linker가 ".OBJ" file에 string을 기록합니다. 이렇게 기록된 string은 
       실행파일내부에 기록되어지며, 이것은 결코 메모리로 load되지 않습니다. 
       하지만 적당한 파일 검색 유틸리티를 사용하여 실행파일에서 string을 
       찾아볼 수 있습니다. 
     * lib 
       ".OBJ" file에 주석의 내용을 기록합니다. 
       library에 새로운 module을 추가하는 경우 에만 comment 프라그마를 사용하여 
       linker에게 결과 file에 명시할 수 있도록 지시할 수 있습니다. 다시 말하면 
       기존에 작성되어진 module에는 comment 프라그마를 사용하여 string을 추가 
       시킬수 없습니다. 새롭게 library를 작성한다면 예외일 수 있겠지요. 
       linker는 최종의 library에서 string에 명시된 library module 이름을 포함 
       합니다. 여러개의 module들도 이름지어질 수 있으며 이름을 만들기 위하여 
       linke되어집니다. 
     * user 
       compiler는 ".OBJ" file에 string을 기록합니다. 하지만 linker에 의해 
       string은 무시되어집니다. object 파일에만 그 내용이 남게 됩니다. 


5. #pragma exit 
   . 사용법 
     #pragma startup function-name   
     #pragma exit function-name   
   . 이들 두 프라그마는 프로그램이 프로그램 시동시(main이 호출되기 전) 호출 
     되어야 할 함수와 프로그램 탈출(프로그램이 _exit를 통해 종료하기 바로 전) 
     을 명시할 수 있도록 합니다. 명시된 function-name은 반드시 인수를 취하지 
     않고 void를 return하는 미리 선언된 함수여야합니다. 다시 말하면 다음과 
     같이 선언될 수 있습니다. 
     void func (void); 
     priority는 반드시 64-255의 범위 내에 있는 정수여야하며 최상의 우선권은 
     0입니다. 0-63사이의 priority는 C library에서 사용하므로 사용자가 이를 
     사용해서는 안됩니다. 최상위 우선권을 가진 함수는 시동시에 맨 먼저 호출 
     되고 탈출시에 맨 마지막으로 호출됩니다. 우선권을 명시해 주지 않을 경우 
     기본적으로 100의 우선권을 갖게 됩니다. pragma startup 또는 exit에 사용된 
     함수명은 반드시 프라그마 라인에 도달하기 전에 정의(또는 선언)되어야함에 
     주의하십시요. 
   ex) 
     #include   
     void startFunc(void) 
     { 
         printf("Startup Function."); 
     } 
     #pragma startup startFunc 64 


     void exit Func(void) 
     { 
         pirntf("Wrapping up execution."); 
     } 
     #pragma exit exitFunc 


     void main(void) 
     { 
         printf("This is main."); 
     } 


6. #pragma hdrfile 
   . 사용법 
     #pragma hdrfile "filename.CSM" 
   . 이 지시어는 프리컴파일된 헤더를 저장할 파일의 이름을 설정합니다. IDE 
     프로젝트를 위한 디폴트 파일명은 .CSM이고 command line용 
     으로는 BC32DEF.CSM이라는 이름을 갖습니다.  프리컴파일된 헤더를 사용하지 
     않으면 이 지시어는 효력이 없으며 명령라인 컴파일러 옵션 -H=filename 또는 
     프리 컴파일된 헤더를 사용하면 프리 컴파일된 헤더를 저장하기 위해 사용되는 
     파일명을 변경할 수 있습니다. 
     명령라인 옵션은 다음과 같습니다. 
     * 프리컴파일드 헤더를 사용하는 경우 
       -H=filename 
     * 프리컴파일드 헤더를 사용은 하지만 새로운 프리컴파일드 헤더파일을 
       변환하지 않는 경우 
       -Hu 
     * 프리컴파일드 헤더를 사용하지 않거나 새로운 프리컴파일드 헤더파일을 
       변환하지 않는 경우. (기본값) 
       -H- 


7. #pragma hdrstop 
   . 사용법 
     #pragma hdrstop 
   . 이 지시어는 프리컴파일에 적합한 헤더 파일의 목록을 종료시키는데, 이것을 
     사용하면 프리컴파일된 헤더가 사용하는 디스크 공간의 양을 줄일 수 있습니다. 
     프리컴파일드 헤더파일은 #pragma hdrstop이 선언되기 전에 #include를 
     사용하여 포함된 헤더파일들을 동일하게 프로젝트 내의 source들간에 공유시킬 
     수 있습니다. 그러므로 #pragma hdrstop전에 일반적인 헤더파일들을 포함하면 
     최상의 콤파일러의 성능을 얻을 수 있습니다. 확실하게 #pragma hdrstop 전에 
     #include를 사용한다면 모든 source file들에게 동일하게 적용되거나 아주 
     조그마한 변화만이 있을 것입니다. IDE 환경에서는 강화된 프리컴파일드 헤더의 
     성능을 가지는 코드로 변환합니다. 예를 들자면 다음의 New Application의 소스 
     파일인 "Unit1.cpp"는 다음과 같이 될것입니다. 


     #include   
     #pragma hdrstop   


     #include "Unit1.h" 
     이 pragma 지시어는 오직 source file에서만 사용하며, 헤더파일에서 사용했다 
     면 아무런 효과도 없을 것입니다. 


8. #pragma inline 
   . 사용법 
     #pragma inline 
   . 이 지시어는 명령 라인 콤파일러 옵션 -B 또는 IDE의 인라인 옵션과 동일 
     합니다. 이것은 컴파일러에게 프로그램 내에 인라인 어셈블리 언어 코드가 
     있음을 알려줍니다. 컴파일러는 #pragma inline을 만날때 -B옵션을 사용하여 
     스스로 재시동하므로 이 지시어는 파일의 상단에 배치되는 것이 최선입니다. 
     실제로 -B옵션과 #pragma inline을 모두 off시켜둘 수 있습니다. 그러면 
     컴파일러는 asm문을 만나자마자 스스로 재시동합니다. 이 옵션과 지시어의 
     목적은 컴파일 시간을 다소 절약하는 것입니다. 


9. #pragma intrinsic 
   . 사용법 
     #pragma intrinsic -function-name 
   . #pragma intrinsic를 사용하면 함수의 inline화를 위해 command-line 스위치나 
     IDE의 옵션이 무시되어집니다.  intrinsic함수를 인라인화할 때는 그 함수를 
     사용하기 전에 반드시 그것을 위한 원형을 포함시켜야만 합니다. 이것은 
     인라인화 할 때 컴파일러가 인라인화한 함수를 내부적으로 인식하는 함수로 
     개명하는 매크로를 실제로 생성하기 때문입니다. 가령 strcpy 함수를 인라인 
     화 하기 위하여 다음과 같은 문장을 사용하였다면 
     #pragma intrinsic strcpy 
     컴파일러는 다음과 같은 매크로를 생성합니다. 
     #define strcpy strcpy 
     컴파일러는 두 선행 밑줄과 두 후미 밑줄을 사용하여 함수 호출을 인식하고 
     그 함수의 원형을 내부적으로 저장해 둔 원형과 부합시키려 합니다. 그러므로 
     원형을 공급하지 않거나 공급한 원형이 콤파일러 내부의 원형과 부합되지 않을 
     경우, 콤파일러는 그 함수를 인라인화 하려는 시도를 불식시키고 에러를 
     발생시킵니다. 이 프라그마 사용의 궁극적인 목적은 함수 호출에 대한 
     오버헤드를 줄위기 위한것입니다. 함수호출은 빨라지겠지만 그만큼 크기는 
     증가하게 될것입니다. 
     ex) 
     #pragma intrinsic strcpy 
     #pragma intrinsic -strcpy 


10.#pragma link 
   . 사용법 
     #pragma link "pathmodulename.ext" 
   . 이 지시어는 실행화일에 파일을 링크시킬것을 링커에세 지시합니다. 
     기본적으로 링커는 -L옵션으로 지정된 패스와 로칼 디렉토리에서 modulename을 
     찾습니다. path 아규먼트를 이용하여 디렉토리를 지정할 수도 있습니다. 또한 
     링커는 확장자를 ".obj"를 기본으로 간주합니다. 


11.#pragma message 
   . 사용법 
     #pragma message ("text" "text"["text" ...]) 
     #pragma message text 
   . #pragma message는 프로그램 코드 내에서 사용자 정의 메세지를 명시합니다. 
     첫번째 형식은 하나 이상의 스트링 상수들로 구성된 문장을 필요로 하고 
     메세지는 괄호안에 싸여있어야만 합니다.(이 형식은 MSC와 호환됩니다.) 
     두번째 형식은 경고 메세지의 문장을 위해 #pragma에 연속되는 문장을 
     사용합니다. #pragma의 두가지 형태와 함께 다른 메크로의 참조는 메세지가 
     디스플레이 되기전에 확장되어집니다.  사용자 정의 메세지가 디스플레이 
     되는것은 기본치이며 명령 라인 옵션의 Show Warnings를 사용하여 
     on/off 시킬 수 있습니다. 이 옵션은 콤파일러의 -wmsg에 해당합니다. 


   ex) 
     #if defined(UNICODE) && !defined(_UNICODE) 
     #ifndef RC_INVOKED 
     #pragma message("MSACM.H: defining _UNICODE 
                      because application defined UNICODE") 
     #endif 
     #define _UNICODE 
     #endif 


     #pragma message osl/ustring.h has been replaced by winsys/string.h 
     #include   


12.#pragma obsolete 
   . 사용법 
     #pragma obsolete identifier 
   . #pragma obsolete 프로그램 코드에서 pragma의 선언 이후에 마주치게 되는 
     identifier의 첫번째 사용에 대해서 경고를 발생합니다. 경고는 identifier를 
     쓸모없는 상태로 만듭니다. 
   ex) 
   #if !defined(RC_INVOKED) 


   #pragma obsolete _chmod 
   #pragma obsolete _close 
   #pragma obsolete _creat 
   #pragma obsolete _open 
   #pragma obsolete _read 
   #pragma obsolete _write 


   #pragma pack(pop) 


   #if defined(STDC) 
   #pragma warn .nak 
   #endif 


   #endif   


13.#pragma option 
   . 사용법 
     #pragma option options 
     #pragma option push options 
     #pragma option pop 
   . #pragma option은 프로그램 원시 코드 내에 명령라인 옵션을 포함시키고자 
     할 때 사용하며 push 또는 pop 옵션과 함께 사용되어질 수 있습니다. 
     options는 임의의 명령라인 옵션(단, 아래에 수록된 것은 제외합니다.)이며 
     하나의 지시어 내에서 여러개의 option들을 나타낼 수 있습니다. 
     예를 들자면 다음과 같습니다. 


     #pragma option -C 
     #pragma option -C -A 


     toggle option(-a, -K같은)은 comman line에서 on/off될수 있습니다. 
     이들 toggle option들은 option 다음에 마침표를 두면 그 명령라인, 구성 파일, 
     옵션 메뉴 설정값에 대해 옵션을 리털할 수 있으며 이를 이용하면 정확한 
     설정값을 기억하지 않고도(혹은 알 필요가 없거나) 옵션을 임시로 변경했다가 
     다시 그것을 디폴트로 복귀시킬 수 있습니다. 


     pragma optino에 포함하여 나타날 수 없는 옵션들은 다음과 같습니다. 
     -B   -c   -dname 
     -Dname=string   -efilename   -E 
     -Fx  -h   -lfilename 
     -lexset   -M   -o 
     -P   -Q   -S 
     -T   -Uname   -V 
     -X   -Y 
     다음의 경우에 #pragmas, #indluces, #define과 약간의 #ifs를 사용할 수 
     있습니다. 
     * #if, #ifdef, #ifndef 또는 #elif지시어 내에서 두 밑줄로 시작하는 매크로명 
       (그리고 그에 따른 내장 매크로도 가능합니다.)의 사용 전. 
     * 첫번째 실재 token이 발생하기 전(첫번째 C 또는 C++ 선언문) 


     특정 명령 라인 옵션은 이들 사건 앞의 #pragma option 내에서만 나타날 수 
     있는데 그러한 option들은 다음과 같습니다. 
     -Efilename        -f      -i# 
     -m*   -npath   -ofilename 
     -u   -W   -z 


     다른 option들은 어디서나 변경될 수 있는데 다음 option들은 함수 또는 대상 
     선언문 사이에서 변경될 경우 컴파일러에만 영향을 미칩니다. 
     -1        -h      -r 
     -2   -k   -rd 
     -a   -N   -v 
     -ff  -O   -y 
     -G   -p   -Z 


     다음의 option들은 언제든지 변경될 수 있으며 즉시 영향을 미칠 수 있습니다. 
     -A   -gn   -zE 
     -b   -jn   -zF 
     -C   -K   -zH 
     -d   -wxxx 
     이들 option들은 그 명령 라인 상태로 재설정하기 위해 점(.)앞에 추가로 
     나타날 수 있습니다. 


     push 또는 pop을 사용한 #pragma option 
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
     또한 콤파일러 지시어들을 쉽게 변경할 수 있도록 push 그리고 pop 아규먼트들 
     과 함께 #pragma option 지시어를 사용할 수도 있습니다. 


     잠재적으로 많은 컴파일러 옵션과 경고들을 변경하는 파일들을 포함하기 위해 
     #pragma option push를 사용할 수 있고, #pragma option pop은 단일 문장으로서 
     이전의 상태를 되돌려준다. 예를 들자면 다음과 같다. 


     #pragma option push 
     #include   
     #pragma option pop 
     #include "mystuff.h" 


     #pragma option push 지시어는 첫번째로 모든 콤파일러 옵션들과 경고 설정들을 
     스택에 push한 후에 다른 옵션들이 존재한다면 이를 처리한다. 다음의 예는 
     #pragma option push가 옵션들을 사용하거나 혹은 그렇지 않을수 있음을 
     보여줍니다. 


     #pragma option push -C -A 
     #pragma option push 


     #pragma option pop directive은 스택으로부터 옵션들과 경고들의 마지막 설정 
     을 pop함으로서 컴파일러 옵션과 경고들을 변경합니다. 만일 스택이 비어있고 
     option pop과 일치하는 option push가 없으며 아무것도 발생하지 않은경우 
     경고가 주어집니다. 다음은 빈 스택에대해서 경고를 발생시킵니다. 


     #pragma option push 
     #pragma option pop 
     #pragma option pop      /* 경고가 발생합니다. 


     권장하지는 않지만 지시어를 사용하여 이 경고를 off시킬 수 있습니다. 
     #pragma warn -nop. 


     만일 pop의 다음에 어떤 옵셥들을 명시할려고 한다면 에러가 발생하게되며 
     pragma option pop 다음에는 어떤것도 허락하지 않습니다. 예를 들면, 다음은 
     에러를 발생합니다. 


     #pragma option pop -C         /* ERROR 
     만일 push된 옵션들의 스택이 파일의 시작과 마지막이 동일하지 않다면 
     다음과 같은 경고메세지가 발생합니다. 


     Previous options and warnings not restored. 


     이 경고메세지를 off시키기 위하여 지시어 #pragma nopushoptwarn를 사용할 
     수 있습니다. 


14.#pragma pack 
   . 사용법 
     #pragma pack(n) 
     #pragma pack(push, n) 
     #pragma pack(pop) 
   . #pragma pack 지시어는 콤파일러 옵션 -a와 함께 #pragma option을 사용하는 
     것과 동일합니다. n은 콤파일러가 저장된 메모리에 데이터를 정렬하는 방법을 
     결정하는 byte의 정렬이다. 보다 자세한 사항은 -a 콤파일러 옵션에 관한 
     내용을 참고하십시요. #pragma pack은 또한 #pragma option지시어에 push나 
     pop을 사용하는것과 동일한 기능을 제공 하도록 push나 pop 아규먼트와 함께 
     사용할 수 있습니다.  아래의 내용은 #pragma pack과 #pragma option을 비교한 
     내용입니다. 
     ━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━ 
     #pragma pack              ┃     #pragma option 
     ━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━ 
     #pragma pack(n)           ┃     #pragma option -an 
     #pragma pack(push, n)     ┃     #pragma option push -an 
     #pragma pack(pop)         ┃     #pragma option pop 
     ━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━ 
15.#pragma package 
   . 사용법 
     #pragma package(smart_init) 
     #pragma package(smart_init, weak) 
   . smart_init 아규먼트 
     #pragma package(smart_init)는 패키지된 유닛들이 의존할 순서를 결정하기위해 
     초기화 되어집니다.(패키지 소스파일 내에 기본적으로 포함됩니다.) 
     일반적으로, 패키지들을 생성하는 .CPP 파일들에 #pragma package를 사용할 수 
     있습니다. 
     이 프라크마는 유닛을 콤파일하는기위한 초기의 순서에 영향을 미칩니다. 
     초기화는 다음의 순서에 의하여 발생합니다. 
     1. 만일 unitA가 unitB에 의존한다면 unitB는 반드시 unitA전에 초기화 
        되어져야만하는 사용("uses")에 의존합니다. 
     2. 링크의 순서(The link order) 
     3. unit에서의 우선권의 순서.(Priority order within the unit.) 


     보통의 .OBJ 파일들은(unit들로 생성하지 않은), 첫째로 우선권의 순서에 따라 
     초기화가 일어나고서 링크가 됩니다. .OBJ 파일들의 링크 순서의 변경은 
     글로발 오브젝트가 호출되어져 생성되는 순서에 의해 변경됩니다. 


     다음의 예는 보통의 .OBJ 파일들과 unit들의 초기화에 어떤 차이점이 있는가를 
     보여줍니다. 세개의 unit 파일들 A,B,C가 #pragma package(smart_init)를 
     사용하여 "smart initialized"되고 우선권은 10, 20, 30의 값을 갖는다고 
     예를 듭니다. 함수는 우선권의 값과 parent .OBJ에 의하여 이름지어져 a10, 
     a20, a30, b10등과 같은 이름을 갖습니다. 세가지는 모두 unit들이며 A는 B와 
     C를 사용하며 A,B,C의 순서로 링크되고 초기화의 순서는 다음과 같습니다. 
          B10 B20 B30 C10 C20 C30 A10 A20 A30 
     위와 같이 되었다면 .OBJ 파일들은 (unit들이 아니다)다음의 순서가 되어질 
     것입니다. 
          A10 B10 C10 A20 B20 C20 A30 B30 C30 
     #pragma package(smart_init)를 사용한 .CPP 파일들은 또한 #pragma package 
     (smart_init)를 정의한 .CPP 파일로부터 다른 .OBJ 파일들을 참조하는 #pragma 
     link를 필요로 하며 unit에 의해 결정되어져야만 합니다. #pragma link는, 결정 
     되지 않은 .OBJ는 라이브러리 등에 의하여 여전히 결정되어질 수 있도록 참조 
     할 수 있습니다. 
   . weak packages 
     #pragma package(smart_init, weak)지시어는 .OBJ 파일이 패키지의 .BPI와 
     .BPL 파일들에 정장되는 방법에 영향을 미칩니다. 만일 #pragma package(smart_ 
     init, weak)가 unit파일 내에 나타난다면 콤파일러는 가능하다면 BPL들로부터 
     unit을 생략하고, 다른 에플리케이션이나 패키지에 의해 필요로 할 때면 
     비 패키지화된(non-packaged) 로칼 복사본의 unit을 생성합니다. 유닛이 이 
     지시어와 함께 콤파일 되었다는 것은 약하게 패키지화 되었음을 이야기 합니다. 
     ("weakly packaged") 
     #pragma package(smart_init, weak)는 동일한 외부 라이브러리(external librar 
     y)들에 의존할수 있는 여러 패키지들 사이에서의 충돌을 제거하는데 사용되어 
     집니다.  #pragma package(smart_init, weak) 지시어를 가지는 unit 파일들은 
     글로발 변수들을 갖지 않아야 합니다. 


16.#pragma resource 
   . 사용법 
     #pragma resource "*.dfm" 
   . 이 프라그마는 form unit에 의해 선정되어지는 파일로서 일치되는 .DFM 파일과 
     헤더파일을 필요로 합니다. 이러한 모든 파일들은 IDE에 의해 관리되어집니다. 
     만일 폼을 위한 다른 변수들을 필요로한다면 pragma resource가 사용되어지고난 
     후에 즉시 선언되어져야만 합니다. 선언은 반드시 form이 되어져야만 합니다. 
         TFormName *Formname; 


17.#pragma startup 
   . 사용법 
     #pragma startup function-name   
     #pragma exit function-name   
   . #pragma exit의 내용을 참조하십시요. 


18.#pragma warn 
   . 사용법 
     #pragma warn +:-:.www 
   . warn지시어를 이용하면 특정 명령라인 옵션 -wxxx를 우선할 수 있습니다. 
     #pragma warn -aus 스위치를 사용하면 함수 단위로 취급됩니다. 개별적인 
     변수들을 위해서 함수 내부에서 경고를 off시킬수는 없습니다. 함수 전체를 
     off시키거나 혹은 그렇지 않거나 둘중 하나입니다. 
   ex) 
   #pragma warn +xxx 
   #pragma warn -yyy 
   #pragma warn .zzz 


   위의 예에서는 xxx경고문은 on되고 yyy경고문은 off되며 zzz경고문은 파일의 
   컴파일이 시작할 때 갖고 있던 값으로 재저장됩니다. 

'컴퓨터 이야기 > C++' 카테고리의 다른 글

파일 다이얼로그  (0) 2010.07.17
Self documenting 합시다!!  (1) 2010.07.17
TR1 간단하지만 긴 소개  (0) 2010.05.25
헤더 파일은 적당히 나눠야 한다  (0) 2010.05.25
f()와 f(void)의 차이점과 활용  (0) 2010.05.25
:

f()와 f(void)의 차이점과 활용

컴퓨터 이야기/C++ 2010. 5. 25. 03:46

이 글은 'C' 에서 입니다. 'C++' 에선 이 문법은 불법입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void foo1()
{
}

void foo2(void)
{
}

int main()
{
    foo1();
    foo2();
    return 0;
}


와 같은 코드는 될까요? 당연히 됩니다. 그렇다면
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void foo1()
{
}

void foo2(void)
{
}

int main()
{
    foo1(1, 2);
    foo2();
    return 0;
}


는 어떨까요? 안될거 같죠? 됩니다. 되니까 글을 썼겠죠. -_-; 근데 이게 된다는건 어딘가에 쓸일이 있는거겠죠. 디버깅 해봅시다. 이제부터 foo2 함수는 더 이상 말하지 않겠습니다. 우리의 초점은 foo1 에 맞춰야 하니까요.
	foo1(1, 2);
0041726E  push        2    
00417270  push        1    
00417272  call        @ILT+2045(_foo1) (411802h) 
00417277  add         esp,8 
으흐흐 역시 우리의 컴파일러는 1 과 2 를 인자로 넣고 있군요! 
일단 간단하게 이걸 이용하는 예제로 int d = foo1(a, b); 를 하면
d = a+b; 를 수행하는 함수를 만들겁니다.

어쨌거나 아래와 같은 코드로
 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
32
33
__declspec(naked) int foo1()
{
    __asm
    {
        push ebp; // ebp 보존을 위한 백업준비 #1


        mov ebp, esp; // ebp 에 esp 를 받아둠.


        sub esp, 0cch; // 내부에서 또 스택을 쓸 수도 있으므로 공간마련 -_-;


    }
    __asm
    {
        mov eax, dword ptr[ebp+8];
        add eax, dword ptr[ebp+0ch];
    }
    __asm
    {
        mov esp,ebp; // sub 로 뺐던 esp 를 다시 원래대로 복원


        pop ebp; // #1


        ret;
    }
}

int main()
{
    printf("%d\n", foo1(1, 2));
    return 0;
}



2005년 어느날 작성

'컴퓨터 이야기 > C++' 카테고리의 다른 글

파일 다이얼로그  (0) 2010.07.17
Self documenting 합시다!!  (1) 2010.07.17
TR1 간단하지만 긴 소개  (0) 2010.05.25
헤더 파일은 적당히 나눠야 한다  (0) 2010.05.25
전 처리기 pragma 키워드  (0) 2010.05.25
:

Duff's device

컴퓨터 이야기 2010. 5. 25. 03:42

Duff's device

Q.

template <typename T>
void fill_array(T* begin, T* end, const T& data) {

	while (begin != end) {
		
		*begin = data;
		++begin;
	}
}

위의 코드와 같은 단순 루프를 더욱 최적화하려면 어떻게 해야 할까요?
switch와 case문의 동작 원리에 대한 이해가 필요합니다.


A.

먼저 문제의 코드를 검토해보도록 하겠습니다.

template <typename T>
void fill_array(T* begin, T* end, const T& data) {
	while (begin != end) {
		*begin = data;
		++begin;
	}
}

위의 코드를 최적화하기 위해 가장 먼저 떠오르는 방법은 mem-계열의 함수들입니다. 만약 T가 char형이라면 memset을, T가 char를 제외한 다른 built-in 타입이라면 memcpy를 적당한 방법으로 사용하면 될 듯 합니다. 하지만 문제는 위의 코드의 T는 사용자가 정의한 class타입이 될수도 있다는 사실입니다. 따라서 다른 방법을 검토해봐야 합니다.

코드는 세줄로 이루어져 있습니다. 그리고 그 중 밑의 두줄은 더 이상 어떻게 최적화할 수 있는 방법이 없어 보입니다. 따라서 "while (begin != end)" 부분을 최적화해야만 합니다. 즉 하나의 데이터를 대입하기 위해 한번의 비교를 하는것보다 한번의 비교에 여러개의 데이터를 대입하면 성능이 더욱 향상된다는 사실을 이용하는 것입니다.

먼저 비교 한번에 8개 정도의 데이터를 대입할 수 있는 코드를 작성해보면 아래와 같이 됩니다.

int m = (end - begin) & 7;
while (m--) {
	*begin = data; ++begin;
}
while (begin != end) {
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
	*begin = data; ++begin;
}

거의 다 해결된 것 같은데 남은 문제는 8의 나머지 수만큼은 역시 하나의 데이터 대입당 한번씩의 비교가 필요하게 됩니다. (코드의 윗부분입니다. 물론 end - begin이 매우 클 경우 무시될 수 있겠지만 이런 것을 그냥 보고만 있을 프로그래머들이 아니겠지요? ^_^)

그래서 나온 해결책이 switch, case문을 사용하는 Duff's device라는 것입니다. 예상하실 수 있듯이 Duff라는 사람이 만들었다고 합니다. (기본 duff's device는 http://www.lysator.liu.se/c/duffs-device.html에서 보실 수 있습니다.)

Duff's device를 사용한 해결책은 아래와 같습니다.

template <typename T>
void fill_array(T* begin, T* end, const T& data) {
	switch ((end - begin) & 7) {
	case 0:	while (begin != end) {
			*begin = data; ++begin;
	case 7:		*begin = data; ++begin;
	case 6:		*begin = data; ++begin;
	case 5:		*begin = data; ++begin;
	case 4:		*begin = data; ++begin;
	case 3:		*begin = data; ++begin;
	case 2:		*begin = data; ++begin;
	case 1:		*begin = data; ++begin;
			}
	}
}

합법적이며 portable한 C++/C코드입니다. 성능이 중요한 루틴에서 실제로 사용되고 있다고 합니다.

참고로 위와 같은 문법이 가능해지는 이유는 switch문에서의 case문은 goto문의 label과 같은 방법으로 사용되기 때문입니다. 예를 들면 아래와 같은 코드는 "default" 대신 "3"을 출력하게 됩니다.

int c = 3;
switch (c) {
case 1:
	cout << "case 1\n";
	{
		case 2:
			cout << "case 2\n";
			{
				case 3:
					cout << "case 3\n";
					break;
			}
			break;			
	}
	break;
default:
		cout << "default\n";
	break;
} 


Duff's device에 대한 실험 결과가 있어서 같이 올립니다. (아래 결과는 제가 직접 실험한 것이 아닙니다.)

사용한 방법은 COUNT 변수만큼의 double 배열을 REPEAT 변수만큼 반복하기를 반복 실험했다고 합니다. 실험 플랫폼은 P-III/750Mhz와 MSVC, MWCW, GNU g++ 2.95.2 컴파일러라고 합니다. (MWCW는 Metroworks CodeWarrior 6.9를 나타냅니다.)

먼저 Fill 알고리즘의 경우, 즉 배열을 어떤 값으로 채우는 경우.

  • COUNT >= 100,000일 경우에는 일반 for-loop와 Duff's Device의 결과는 실용적으로 봤을때 거의 같다.
     
  • COUNT == 1,000일 경우에는 Duff's Device가 20% 향상이 있었다.
     
  • COUNT == 100일 경우 Duff's Device가 12%의 향상이 있었다.
     

(물론 위의 값들은 정확하지 않을 수 있으며 MSVC에서의 Duff's device에 관한 내용만 정리해본 것입니다.)

COUNT 값이 매우 클 경우 왜 알고리즘에 따른 차이가 없는지는 다음 원인 때문이라고 추정하고 있습니다.

  • 현재 CPU들은 대부분 L1, L2 cache들을 가지고 있다. 이러한 cache들은 main memory보다 수배에서 수십배 더 빠르게 동작한다. 여기서 data access시의 locality는 cache management에 매우 큰 영향을 미치게 된다. 하지만 위의 1번의 경우처럼 데이터의 양이 많은 경우는 cache hit 실패율이 매우 높아지기 때문에 루프의 성능은 알고리즘보다는 main memory의 bandwidth값에 수렴하게 된다. 즉 모두 비슷한 결과를 보이게 된다. 하지만 2, 3번과 같은 경우에는 성능에 알고리즘이 영향을 미치는 비율이 높아지며 따라서 알고리즘에 따라 조금씩 다른 결과를 보이게 된다.

위의 Fill 알고리즘외에 배열끼리의 Copy 알고리즘에 대해서도 쓰여 있는데 Duff's device가 역시 일반 루프보다는 나은 성능을 보여주고 있습니다.

얘기의 마무리를 하자면 Duff's device는 일반 루프보다 (거의 항상) 나은 성능을 나타내며 모든 타입에 대해서 사용 가능하고 100% portable한 코드가 됩니다.


참고로 이 실험과 관련된 원본 문서는 http://www.cuj.com/experts/1910/alexandr.htm에 있습니다.


2002.3.1. 잘못된 부분에 대해서는 에게 메일 부탁드립니다.

출처
: