최신 C ++로 포팅하는 키 / 밸류 스토어 개발


9

Cassandra와 비슷한 데이터베이스 서버를 개발 중입니다.

C로 개발이 시작되었지만 클래스 없이는 상황이 매우 복잡해졌습니다.

현재 C ++ 11로 모든 것을 이식했지만 여전히 "현대"C ++를 배우고 있으며 많은 것들에 대해 의문이 있습니다.

데이터베이스는 키 / 값 쌍으로 작동합니다. 모든 쌍에는 추가 정보가 있습니다. 만기 시점 (만료되지 않은 경우 0). 각 쌍은 변경할 수 없습니다.

키는 C 문자열이고 값은 void *이지만 적어도 C 문자열과 같은 값으로 작동하는 순간은 아닙니다.

추상 IList클래스가 있습니다. 세 클래스에서 상속

  • VectorList -C 동적 배열-std :: vector와 유사하지만 realloc
  • LinkList -점검 및 성능 비교를 위해 제작
  • SkipList -마침내 사용될 클래스.

앞으로 나는 Red Black나무도 할 수 있습니다.

각각 IList은 키로 정렬 된 0 개 이상의 쌍에 대한 포인터 를 포함합니다 .

경우 IList너무 오래되었고, 그것은 특별한 파일의 디스크에 저장할 수 있습니다. 이 특수 파일은 일종입니다 read only list.

키를 검색해야하는 경우

  • 메모리에서 첫 번째 IList가 검색됩니다 ( SkipList, SkipList또는 LinkList).
  • 그런 다음 날짜별로 정렬 된
    파일 (최신 파일부터 가장 오래된 파일-마지막)로 검색을 보냅니다 .
    이 파일들은 모두 메모리에 저장되어 있습니다.
  • 아무것도 찾지 못하면 키를 찾을 수 없습니다.

나는 IList물건 의 구현에 대해 의심의 여지가 없습니다 .


현재 나를 괴롭히는 것은 다음과 같습니다.

쌍은 함께있는 다른 크기, 그들에 의해 할당 new()하고 그들이 가지고있는 std::shared_ptr그들에게 지적했다.

class Pair{
public:
    // several methods...
private:
    struct Blob;

    std::shared_ptr<const Blob> _blob;
};

struct Pair::Blob{
    uint64_t    created;
    uint32_t    expires;
    uint32_t    vallen;
    uint16_t    keylen;
    uint8_t     checksum;
    char        buffer[2];
};

"버퍼"멤버 변수는 크기가 다른 변수입니다. 키 + 값을 저장합니다.
예를 들어, 키가 10 자이고 값이 10 바이트 인 경우 전체 객체가됩니다 sizeof(Pair::Blob) + 20(버퍼는 2 개의 널 종료 바이트로 인해 초기 크기가 2 임).

동일한 레이아웃이 디스크에서도 사용되므로 다음과 같이 할 수 있습니다.

// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];

// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);

// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);

그러나이 다른 크기는 C ++ 코드가있는 많은 장소에서 문제가됩니다.

예를 들어 나는 사용할 수 없습니다 std::make_shared(). 1M 쌍이 있으면 2M 할당이 있기 때문에 이것은 나에게 중요합니다.

다른 쪽에서 동적 배열 (예 : 새로운 char [123])에 "버퍼"를 수행하면 mmap "트릭"을 잃게됩니다. 키를 확인하려면 두 가지 역 참조를해야하며 단일 포인터를 추가합니다 -클래스에 8 바이트

또한 "풀"모든 회원들에게 노력 Pair::BlobPair그래서, Pair::Blob그냥 버퍼로,하지만 난 그것을 테스트 할 때, 아마 때문에 주위의 객체 데이터를 복사하는 매우 느렸다.

내가 생각하고있는 또 다른 변화는 Pair클래스 를 제거하고 클래스를 바꾸고 std::shared_ptr모든 메소드를 다시 "푸시"하는 Pair::Blob것입니다. 그러나 이것은 가변 크기 Pair::Blob클래스에 도움이되지 않습니다 .

더 C ++ 친화적으로 만들기 위해 객체 디자인을 어떻게 개선 할 수 있는지 궁금합니다.


전체 소스 코드는 다음과 같습니다.
https://github.com/nmmmnu/HM3


2
왜 사용하지 않는 std::mapstd::unordered_map? 값 (키와 연관된)이 왜 void*어떤가요? 언젠가는 그것들을 파괴해야 할 것입니다. 어떻게 그리고 언제? 왜 템플릿을 사용하지 않습니까?
Basile Starynkevitch

std :: map을 사용하지 않습니다. 현재 사례에 대해 std :: map보다 더 나은 것을 수행한다고 생각하기 때문입니다. 그러나 네, 어느 시점에서 std :: map을 감싸고 IList로 성능을 확인하려고 생각하고 있습니다.
Nick

할당 해제 및 호출 d-tor는 element가 IList::remove있거나 IList가 파괴 될 때 수행됩니다 . 시간이 많이 걸리지 만 별도의 스레드로 수행하려고합니다. std::unique_ptr<IList>어쨌든 IList가 있기 때문에 쉬울 것 입니다. 새 목록으로 "전환"하고 d-tor라고 부를 수있는 어딘가에 오래된 개체를 유지할 수 있습니다.
Nick

템플릿을 사용해 보았습니다. 사용자 라이브러리가 아니고 키는 항상 C string있고 데이터는 항상 일부 버퍼 void *또는 char *이므로 char 배열을 전달할 수 있으므로 여기에서 가장 좋은 해결책은 아닙니다 . redis또는 에서 비슷한 것을 찾을 수 있습니다 memcached. 어떤 시점 std::string에서 키에 고정 문자 배열 을 사용 하거나 고정 하기로 결정할 수 있지만 밑줄은 여전히 ​​C 문자열입니다.
Nick

6
댓글 4 개를 추가하는 대신 질문을 편집해야합니다.
Basile Starynkevitch

답변:


3

내가 권유하는 방법은 키-밸류 저장소의 인터페이스에 집중하여 가능한 한 깨끗하고 가능한 한 제한이 없도록하는 것입니다. 즉, 발신자에게 최대한의 자유를 제공하고 선택의 자유를 극대화해야합니다. 그것을 구현하는 방법.

그런 다음 성능에 영향을주지 않으면 서 최대한 간결하고 가능한 한 깨끗한 구현을 제공하는 것이 좋습니다. 나에게 그것은 unordered_map당신의 첫 번째 선택이거나 아마도 map어떤 종류의 키 순서가 인터페이스에 노출 되어야 하는 것처럼 보입니다 .

따라서 먼저 깨끗하고 최소한으로 작동하도록하십시오. 그런 다음 실제 응용 프로그램에 사용하십시오. 그렇게하면 인터페이스에서 해결해야 할 문제를 찾을 수 있습니다. 그런 다음 계속 진행하십시오. 대부분의 경우 인터페이스 변경으로 인해 구현의 큰 부분을 다시 작성해야하므로 구현에 필요한 최소한의 시간을 넘어서 구현의 첫 번째 반복에 이미 투자 한 경우 간신히 시간이 낭비됩니다.

그런 다음 프로파일을 작성하고 인터페이스를 변경하지 않고 구현에서 개선해야 할 사항을 확인하십시오. 또는 프로파일 링하기 전에 구현을 개선하는 방법에 대한 자신의 아이디어가있을 수 있습니다. 괜찮습니다. 그러나 아직 이러한 아이디어를 연구 할 이유가 없습니다.

당신은 당신이 더 잘하고 싶다고 말합니다 map. 그것에 대해 말할 수있는 두 가지가 있습니다 :

a) 아마 그렇지 않을 것입니다.

b) 모든 비용에서 조기 최적화를 피하십시오.

구현과 관련하여 주요 문제는 메모리 할당으로 보입니다. 메모리 할당과 관련하여 예상되는 문제를 해결하기 위해 디자인을 구성하는 방법에 관심이있는 것 같습니다. C ++에서 메모리 할당 문제를 해결하는 가장 좋은 방법은 적절한 메모리 할당 관리를 구현하는 것입니다. Java 및 C #과 같은 언어와 달리 언어 런타임이 제공하는 것에 거의 얽매이지 않는 자체 메모리 할당 관리를 수행 할 수있는 C ++을 사용하고 있다는 점을 행운으로 생각해야합니다.

C ++에서 메모리 관리를 수행하는 방법에는 여러 가지가 있으며 new연산자 를 오버로드하는 기능이 유용 할 수 있습니다. 프로젝트를위한 단순한 메모리 할당자는 대량의 바이트 배열을 미리 할당하여 힙으로 사용합니다. ( byte* heap) firstFreeByte인덱스가 0으로 초기화되어 힙의 첫 번째 사용 가능한 바이트를 나타냅니다. N바이트 요청이 들어 오면 주소를 반환하고에 heap + firstFreeByte추가 N합니다 firstFreeByte. 따라서 메모리 할당이 너무 빠르고 효율적으로되어 사실상 문제가되지 않습니다.

물론, 모든 메모리를 미리 할당하는 것은 좋은 생각이 아닐 수 있으므로 힙을 요청시 할당 된 뱅크로 나누고 주어진 모든 은행에서 할당 요청을 계속 제공해야 할 수도 있습니다.

데이터는 불변이므로 좋은 솔루션입니다. 가변 길이 객체에 대한 아이디어를 포기 Pair하고 데이터에 대한 추가 메모리 할당에 거의 비용이 들지 않으므로 각각 데이터에 대한 포인터를 포함하도록 할 수 있습니다.

힙에서 객체를 버리고 메모리를 회수하려면 상황이 더 복잡해집니다. 포인터가 아닌 포인터를 사용해야하므로 항상 객체를 이동할 수 있습니다 삭제 된 객체의 공간을 회수하기 위해 힙에서 추가적인 간접 지향으로 인해 모든 것이 조금 느려지지만 표준 런타임 라이브러리 메모리 할당 루틴을 사용하는 것에 비해 모든 것이 여전히 번개처럼 빠릅니다.

그러나 데이터베이스의 단순하고 최소한의 작업 버전을 먼저 작성하지 않고 실제 응용 프로그램에 사용하지 않으면 걱정할 필요가 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.