실제로 표준 세트 컨테이너는 대부분 쓸모가 없으며 배열을 사용하는 것을 선호하지만 다른 방식으로 수행합니다.
집합 교차점을 계산하기 위해 첫 번째 배열을 반복하고 요소를 단일 비트로 표시합니다. 그런 다음 두 번째 배열을 반복하고 표시된 요소를 찾습니다. Voila, 해시 테이블보다 작업 및 메모리가 훨씬 적은 선형 시간으로 교차점을 설정하십시오. 내 코드베이스는 요소를 복제하지 않고 요소 색인을 중심으로 회전하고 (요소 자체가 아닌 요소에 대한 인덱스를 복제 함) 정렬 할 필요가 거의 없지만 몇 년 동안 세트 데이터 구조를 사용하지 않았습니다. 결과.
또한 요소가 그러한 목적으로 데이터 필드를 제공하지 않는 경우에도 사용하는 악의적 인 비트 인증 C 코드가 있습니다. 트래버스 된 요소를 표시하기 위해 가장 중요한 비트 (사용하지 않은 비트)를 설정하여 요소 자체의 메모리를 사용합니다. 실제로는 거의 조립 수준에서 작업하지 않는 한 그렇게하지 마십시오. 그러나 요소가 순회에 특정 필드를 제공하지 않는 경우에도 적용 할 수있는 방법을 언급하고 싶었습니다. 특정 비트는 사용되지 않습니다. 내 딩키 i7에서 1 초 이내에 2 억 개의 요소 (2.4 기가 데이터)에 대한 설정된 교차점을 계산할 수 있습니다. std::set
같은 시간에 각각 1 억 개의 요소를 포함하는 두 인스턴스 간에 교차를 설정하십시오 . 가까이 가지도 않습니다.
옆으로 ...
그러나 각 요소를 다른 벡터에 추가하고 요소가 이미 존재하는지 확인하여이를 수행 할 수도 있습니다.
새로운 벡터에 요소가 이미 존재하는지 확인하는 것은 일반적으로 선형 시간 연산이 될 것이며, 이는 설정된 교차점 자체를 2 차 연산으로 만듭니다 (폭발적인 작업량이 입력 크기가 클수록). 평범한 오래된 벡터 또는 배열을 사용하고 확장 가능한 방식으로 수행하려는 경우 위의 기술을 권장합니다.
기본적으로 어떤 종류의 알고리즘에 집합이 필요하며 다른 컨테이너 유형으로 수행해서는 안됩니까?
컨테이너 작업 수준에서 (세트 작업을 효율적으로 제공하기 위해 특별히 구현 된 데이터 구조에서와 같이) 의견에 대해 편견을 갖고 있지만 개념 수준에서 설정 논리를 필요로하는 것이 많이 있습니다. 예를 들어, 게임 세계에서 비행과 수영을 모두 할 수있는 생물을 찾고 싶다고하는데 한 세트 (실제로 세트 컨테이너를 사용하는지 여부)와 다른 생물에서 수영 할 수있는 생물을 가지고 있다고 가정 해 봅시다. . 이 경우 교차로를 설정해야합니다. 날 수 있거나 마법 같은 생물을 원한다면, 집합 조합을 사용합니다. 물론 이것을 구현하기 위해 실제로 세트 컨테이너가 필요하지 않으며 가장 최적의 구현은 일반적으로 세트로 특별히 설계된 컨테이너가 필요하지 않거나 원하지 않습니다.
탄젠트 해제
지미 제임스로부터이 교차로 접근에 관해 좋은 질문을 받았습니다. 그것은 주제를 다소 벗어 났지만 오, 더 많은 사람들 이이 기본 침입 방식을 사용하여 교차를 설정하여 균형 잡힌 이진 트리 및 해시 테이블과 같은 전체 보조 구조를 설정 작업의 목적으로 작성하지 않도록하는 데 관심이 있습니다. 언급 한 바와 같이, 근본적인 요구 사항은리스트가 얕은 카피 요소를 색인화하거나 정렬되지 않은 첫 번째 정렬되지 않은리스트 또는 배열을 통과하거나 "두 번째에서 픽업 할 때"표시 될 수있는 공유 요소를 가리 키도록하는 것입니다. 두 번째 목록을 통과하십시오.
그러나 다음과 같은 경우에는 요소를 건드리지 않고도 멀티 스레딩 컨텍스트에서도 실질적으로 달성 할 수 있습니다.
- 두 집계에는 요소에 대한 인덱스가 포함됩니다.
- 지수의 범위는 너무 크지 않으며 (예 : [0, 2 ^ 26), 수십억 이상이 아님) 합리적으로 밀집되어 있습니다.
이를 통해 집합 연산을 위해 병렬 배열 (요소 당 하나의 비트) 만 사용할 수 있습니다. 도표:
스레드 동기화는 풀에서 병렬 비트 배열을 가져 와서 풀로 다시 릴리스 할 때만 필요합니다 (범위를 벗어날 때 암시 적으로 수행됨). 설정 작업을 수행하기위한 실제 두 루프에는 스레드 동기화가 필요하지 않습니다. 스레드가 비트를 로컬로 할당하고 해제 할 수있는 경우 병렬 비트 풀을 사용할 필요조차 없지만, 비트 풀은 중앙 요소가 자주 참조되는 이런 종류의 데이터 표현에 맞는 코드베이스에서 패턴을 일반화하는 데 편리 할 수 있습니다. 각 스레드가 효율적인 메모리 관리를 방해하지 않도록 인덱스별로 내 영역의 주요 예는 엔터티 구성 요소 시스템 및 인덱스 메쉬 표현입니다. 둘 다 종종 교집합이 필요하며 중앙에 저장된 모든 것을 참조하는 경향이 있습니다 (ECS와 정점, 모서리,
인덱스가 밀집되어 있고 드문 드문 흩어져있는 경우에도 병렬 비트 / 부울 배열의 합리적인 스파 스 구현 (해당 512 비트 청크 (풀린 노드 당 64 바이트, 512 개의 연속 인덱스를 나타내는 노드) 만 저장)과 같이 적용 할 수 있습니다. ) 및 완전히 빈 연속 블록 할당을 건너 뜁니다. 중앙 데이터 구조가 요소 자체에 의해 거의 사용되지 않는 경우 이미 이와 같은 것을 사용하고있을 가능성이 있습니다.
... 희소 비트 세트가 병렬 비트 배열로 사용되는 비슷한 아이디어. 이러한 구조는 새로운 불변 복사를 만들기 위해 깊게 복사 할 필요가없는 청키 블록을 얕게 복사하기 쉽기 때문에 불변성에 적합합니다.
평균적인 기계에서이 접근 방식을 사용하여 수억 개의 요소들 사이의 교차점을 1 초 이내에 완료 할 수 있으며, 이는 단일 스레드 내에 있습니다.
클라이언트가 결과 교차점에 대한 요소 목록이 필요하지 않은 경우 절반 미만으로 수행 할 수 있습니다. 예를 들어 두 목록에서 찾은 요소에 일부 논리 만 적용하려는 경우에는 통과 할 수 있습니다 함수 포인터 또는 functor 또는 delegate 또는 교차하는 요소의 범위를 처리하기 위해 다시 호출되는 모든 것. 이 효과에 뭔가 :
// 'func' receives a range of indices to
// process.
set_intersection(func):
{
parallel_bits = bit_pool.acquire()
// Mark the indices found in the first list.
for each index in list1:
parallel_bits[index] = 1
// Look for the first element in the second list
// that intersects.
first = -1
for each index in list2:
{
if parallel_bits[index] == 1:
{
first = index
break
}
}
// Look for elements that don't intersect in the second
// list to call func for each range of elements that do
// intersect.
for each index in list2 starting from first:
{
if parallel_bits[index] != 1:
{
func(first, index)
first = index
}
}
If first != list2.num-1:
func(first, list2.num)
}
또는이 효과에 무언가. 첫 번째 다이어그램에서 의사 코드의 가장 비싼 부분은 intersection.append(index)
두 번째 루프 std::vector
에 있으며 작은 목록의 크기에 미리 예약되어있는 경우에도 적용됩니다 .
모든 것을 딥 카피하면 어떻게됩니까?
그만 해요! 교차점을 설정해야하는 경우 교차 할 데이터를 복제하고 있음을 의미합니다. 아주 작은 객체조차도 32 비트 인덱스보다 작을 수 있습니다. 실제로 인스턴스화 된 ~ 43 억 개 이상의 요소가 필요하지 않으면 요소의 주소 지정 범위를 2 ^ 32 (2 ^ 32 바이트가 아닌 2 ^ 32 요소)로 줄일 수 있습니다.이 시점에서 완전히 다른 솔루션이 필요합니다 ( 메모리에 설정된 컨테이너를 사용하지 않는 것이 확실합니다).
주요 경기
요소가 동일하지 않지만 일치하는 키를 가질 수있는 작업을 설정해야하는 경우는 어떻습니까? 이 경우 위와 동일한 아이디어입니다. 각 고유 키를 인덱스에 매핑하면됩니다. 예를 들어 키가 문자열 인 경우, 인터 닝 된 문자열이이를 수행 할 수 있습니다. 이 경우 문자열 키를 32 비트 인덱스에 매핑하기 위해 trie 또는 해시 테이블과 같은 멋진 데이터 구조가 필요하지만 결과 32 비트 인덱스에 대해 설정 작업을 수행하기 위해 이러한 구조가 필요하지 않습니다.
기계의 전체 주소 범위가 아닌 매우 합리적인 범위의 요소에 대한 색인으로 작업 할 수있을 때 매우 저렴하고 간단한 알고리즘 솔루션과 데이터 구조가 많이 열립니다. 따라서 가치가있는 것보다 더 많습니다 각 고유 키에 대한 고유 색인을 얻을 수 있습니다.
나는 인덱스를 좋아한다!
나는 피자와 맥주만큼이나 인덱스를 좋아합니다. 내가 20 대에있을 때, 나는 실제로 C ++에 들어가서 모든 종류의 표준 호환 데이터 구조 (컴파일 타임에 범위 ctor에서 채우기 ctor를 명확하게하는 데 필요한 트릭 포함)를 설계하기 시작했습니다. 돌이켜 보면 그것은 많은 시간 낭비였습니다.
데이터베이스를 배열로 중앙 집중식으로 저장하고 조각화하고 기계의 전체 주소 지정 가능 범위에 걸쳐 저장하는 대신 요소를 색인화하는 데 데이터베이스를 돌리면 알고리즘 및 데이터 구조 가능성의 세계를 탐색 할 수 있습니다. 주변에 오래된 일반 회전 컨테이너와 알고리즘 설계 int
또는 int32_t
. 그리고 최종 결과가 한 데이터 구조에서 다른 데이터 구조로 다른 데이터 구조로 다른 요소로 다른 요소로 지속적으로 전송하지 않는 경우 훨씬 더 효율적이고 유지 관리하기 쉽다는 것을 알았습니다.
일부 고유 한 값에 T
고유 인덱스가 있고 중앙 배열에 인스턴스가 있다고 가정 할 수있는 사용 사례 예 :
인덱스에 대한 부호없는 정수와 잘 작동하는 멀티 스레드 기수 정렬 . 실제로 다중 스레드 기수 정렬을 사용하면 Intel의 병렬 정렬과 같은 1 억 개의 요소를 정렬하는 데 약 1/10의 시간이 걸리며 Intel은 이미 std::sort
큰 입력 보다 4 배 빠릅니다 . 물론 인텔은 비교 기반 정렬이기 때문에 훨씬 유연하며 사전을 정렬 할 수 있으므로 사과를 오렌지와 비교합니다. 그러나 캐시 친화적 인 메모리 액세스 패턴을 달성하거나 복제본을 빠르게 필터링하기 위해 기수 정렬 패스를 수행 할 수있는 것처럼 종종 오렌지 만 필요합니다.
노드 당 힙 할당없이 링크 된 목록, 트리, 그래프, 별도의 체인 해시 테이블 등과 같은 링크 된 구조를 빌드 할 수 있습니다 . 요소와 병렬로 노드를 대량으로 할당하고 인덱스와 함께 연결할 수 있습니다. 노드 자체는 다음 노드에 대한 32 비트 색인이되어 다음과 같이 큰 배열로 저장됩니다.
병렬 처리에 적합합니다. 연결된 구조는 병렬 처리에 적합하지 않습니다. 왜냐하면 배열을 통한 병렬 for 루프를 수행하는 것과는 반대로 트리 또는 연결된 목록 순회에서 병렬 처리를 시도하는 것이 어색하기 때문에 어색합니다. 인덱스 / 중앙 배열 표현을 사용하면 항상 해당 중앙 배열로 가서 모든 것을 병렬 병렬 루프로 처리 할 수 있습니다. 일부만 처리하려는 경우에도이 방법으로 처리 할 수있는 모든 요소의 중앙 배열이 항상 있습니다 (중앙 배열을 통한 캐시 친화적 액세스를 위해 기수 정렬 목록으로 색인화 된 요소를 처리 할 수 있음).
상수 시간에 즉시 각 요소에 데이터를 연결할 수 있습니다 . 위의 병렬 비트 배열의 경우와 같이 병렬 데이터를 요소에 쉽고 저렴하게 연결하여 임시 처리 할 수 있습니다. 여기에는 임시 데이터 이외의 사용 사례가 있습니다. 예를 들어, 메쉬 시스템은 사용자가 원하는만큼 UV 맵을 메쉬에 첨부 할 수 있도록 할 수 있습니다. 그러한 경우, 우리는 AoS 접근 방식을 사용하여 모든 단일 정점과 얼굴에 얼마나 많은 UV 맵이 있는지 하드 코딩 할 수 없습니다. 우리는 그러한 데이터를 즉석에서 연관시킬 수 있어야하며, 병렬 배열은 거기에서 편리하고 모든 종류의 정교한 연관 컨테이너, 심지어 해시 테이블보다 훨씬 저렴합니다.
물론 병렬 배열은 오류가 발생하기 쉬운 병렬 배열이 서로 동기화되어 있기 때문에 눈살을 찌푸립니다. 예를 들어 "root"배열에서 인덱스 7의 요소를 제거 할 때마다 "children"에 대해서도 동일한 작업을 수행해야합니다. 그러나 대부분의 언어에서이 개념을 범용 컨테이너로 일반화하는 것은 쉬운 일이므로 병렬 배열을 서로 동기화하기위한 까다로운 논리는 전체 코드베이스의 한 곳에만 있으면되며 이러한 병렬 배열 컨테이너는 이후 삽입시 회수 할 배열의 빈 공간에 많은 메모리를 낭비하지 않도록 위의 희소 배열 구현을 사용하십시오.
더 정교함 : 스파 스 비트 셋 트리
좋아, 나는 냉소적이라고 생각하는 것을 좀 더 정교하게 요청했지만 어쨌든 그렇게 재미있을 것입니다! 사람들이이 아이디어를 완전히 새로운 수준으로 끌어 내려면 N + M 요소를 선형 적으로 반복하지 않고도 교차로를 설정하는 것이 가능합니다. 이것은 내가 오랫동안 사용하고 기본적으로 모델로 사용했던 최고의 데이터 구조입니다 set<int>
.
두리스트의 각 요소를 검사하지 않고도 세트 교차를 수행 할 수있는 이유는 계층의 루트에있는 단일 세트 비트가 세트에 백만 개의 인접한 요소가 있음을 나타낼 수 있기 때문입니다. 1 비트 만 검사하면 범위에있는 N 개의 인덱스가 [first,first+N)
세트에 있고 N이 매우 큰 수임을 알 수 있습니다.
점령 된 인덱스를 트래버스 할 때 실제로 이것을 루프 최적화 프로그램으로 사용합니다. 세트에 8 백만 개의 인덱스가 있다고 가정하기 때문입니다. 일반적으로이 경우 메모리의 8 백만 정수에 액세스해야합니다. 이것으로 잠재적으로 몇 비트를 검사하고 점유 된 인덱스의 인덱스 범위를 통해 루프를 돌릴 수 있습니다. 또한, 인덱스의 범위는 정렬 된 순서로되어있어 원래의 요소 데이터에 액세스하는 데 사용되는 정렬되지 않은 인덱스 배열을 반복하는 것과는 대조적으로 캐시 친화적 인 순차 액세스가 가능합니다. 물론이 기법은 매우 드문 경우에 더 나 빠지며 최악의 시나리오는 모든 단일 인덱스가 짝수 (또는 홀수) 인 경우가 많으며,이 경우 연속 영역이 전혀 없습니다. 그러나 내 유스 케이스에서는 최소한