metadata 중에는 tag라는 정수가 있다. 이를 기반으로 다른 table에서 특정 key k가 저장되는 bucket을 파악한다.
방식을 알기 전에 먼저 h라고, [0, S*t)의 범위를 가지는 함수가 있다. input은 key. S는 각 table의 bucket 개수, t는 tag 범위를 결정하는 숫자로 논문에서는 16을 사용.
h(k)의 결과물을 B(k)랑 T(k)로 나누는데, B(k)는 h(k)/t에 floor 적용 값으로 범위는 [0,S). T(k)는 h(k) mod t로 범위는 [0, t). 여기서 T(k)값을 tag라고 하며, t가 tag 범위를 결정하는 숫자인 이유도 이걸로 알 수 있다.
이후 k라는 key가 두 table에서 어느 bucket에 대응되는지는 다음 공식을 통해 결정된다.
여기서 f는 [0,t)→[0,S)의 범위를 가지는 랜덤 함수이며, mod S는 B(k) + f(T(k))에 대해서 적용한다는 점 참고.
모든 entry는 tag랑 B1(k)인지 B2(k)인지를 표기하는 bit, 이렇게 2가지를 보관을 한다. 그러면 만일 relocation이 필요할 경우, 일단 현재 bucket 값이 어디에 해당되는지 bit로 확인을 한 다음 f(T(k))를 각 경우에 대해 적절히 연산을 해서 다른 위치를 구하면 된다.
그런데 그냥 hash function을 2개 쓰면 되지 않나… 라는 생각이 들었다. 이러면 tag 보관도 필요 없고 그냥 어떤 hash function을 썼는지에 대한 bit만 표기하면 되지 않나…? → jump node 관리가 복잡해짐, 충돌 너무 많이 발생, 탐색에서 full key를 활용하지 않는 구조를 만들려고 함의 복합적 결과.
prefix에 대한 bucket을 찾은 다음, 그 안의 특정 entry가 실제 그 prefix에 대한 entry인지 확인할 때 활용할 수 있는 특징이 있다. 바로 그 entry의 parent가, 우리가 미리 찾은 parent랑 동일할 것이라는 점.
이를 활용해 다음을 통해 특정 entry와 관련된 key인 k가 prefix p에 대한 entry인지 확인이 가능.
마지막 symbol은 metadata 형태로 저장을 한다. 문제는 동일 parent를 어떻게 판별하냐는 것이다. 여기서 peelable hash function의 특징을 활용한다.
위는 peelable hash function의 정의 및 Cuckoo Trie에서 사용한 (peelable) hash function과 peeling에 이용되는 함수 Ph의 정의다. 여기서 x*c는 concatenation이며, c가 마지막 symbol,x는 원래 key의 마지막 symbol 제외 prefix 부분이라고 생각하면 된다.
여기에 color이라는 것을 활용하는데 bucket 내의 entry들에 대한 구별에 사용이 된다. B개의 entry를 가지는 bucket에 대해 color의 개수는 2B개가 된다. 이때 사용되는 bit의 개수는 log2(2B)
그러면 relocation 한정 활용되는 metadata는 last_symbol
,color
, parent_color
과 앞의 Relocation부분에서 정의한 tag가 활용된다.
이를 활용해서 특정 bucket의 k라는 key를 가지는 entry가 p라는 prefix와 동일한것을 k를 저장하지 않고 판별하는 방법은 다음과 같다.
last_symbol
을 활용하면 trivialcolor
이 k entry의 parent_color
이랑 동일한지를 확인하면 된다.parent_color
과 미리 구했을 p[:#p-1]의 color을 비교해야 둘이 같은 entry를 가리킨다는 것을 확인할 수 있다.각 table에 대해 hash function을 2개 쓰지 않는 이유도 이거와 관련된 것으로 추정중. 2개의 독립적인 hash function을 사용하면 parent 확인을 제대로 하기 힘들어서. 왜냐하면 어찌저찌 peelable hash function 특징을 활용해 앞부분 hash value를 확인을 했어도, 이 hash value를 활용해 parent가 table에 저장되지 않았을 수 있기 때문이다. 다른 table에 저장된 경우에 말이다. 그래서 두 table에 대해 공통 hash function을 일단 활용한 다음에 모종의 조작을 통해 2개의 table에 적절히 분배하는 형식을 택한 것으로 보인다. 이 방식이 일으킬 side effect에 대해선 잘 모르겠음.
parent_color
last_symbol
depth
라는 변수를 활용한다. 이 값은 평소엔 0인데, jump node를 탐색하는 경우에는 추후 설명할 Algorithm2의 FindChild
에서 이 값을 기준으로 jump node에서 보관하는 symbol들 sequence를 어디까지 탐색하는데 추적하는데 이용된다.SearchByColor
이라는 알고리즘을 통해 다음 child를 구한다. 자세한 과정은 밑의 Algorithm 2 부분 설명 참고.Lookups
Range (사실 file indexing과 관련이 없…나요? batch I/O에서는 활용할 수도 있지 않을…까요?)
k1
과 k2
사이의 key들에 대한 value 구하기.
그럴려면 먼저 자료 구조 상에 ‘실제로’ 존재하는 시작 key를 알아야 한다. 위의 k1
은 그냥 기준용 key이기 때문
먼저 k1
이 존재하는지를 trie상에서 찾는다. Algorithm1 활용. 만약 없을 경우, predecessor search 알고리즘을 활용, 이전 node가 right sibling인 predecessor을 찾을 때까지 올라간 다음, 거기의 left sibling으로 이동, 이후 해당 sibling기반 subtree의 maximal leaf로 이동한다.
이걸 하는 이유는, 일단 start key가 k1
보다 크거나 같은걸 만족하는 애들 중 가장 작은 trie 내 key라는 점에 유의하자. k1
이 존재하지 않으면 탐색 ‘도중’에 멈추게 될 확률이 높다. 일단 이 점 때문에 next locator 기반의 linked list로 start key를 찾는것은 안된다.
그러면 일단 현재 node 기준으로 위에 말한 predecessor search 알고리즘을 기반으로 leaf를 찾아야 한다. 그 다음에 거기서 next leaf locator을 통해 start key를 구해야 한다.
이 때 위로 올라가는 메모리 접근은 이미 접근한 node들이기 때문에 cache에 관련 정보가 있을거라 탐색이 상대적으로 빠르지만, 이후 left sibling 이동 후 거기 subtree의 maximal leaf로 이동하는 것은 접근한 적이 없기 때문에 메모리 접근이 많을 수록 느리다. 그래서 Jump랑 Regular node는 max leaf in subtree locator을 기반으로 빠르게 탐색할 수 있도록 유도한다.
이후 효율적으로 key-value pair을 구할 수 있어야 한다.
위의 구조에선 다음 key의 value를 미리 구하는 것이 불가능하다. 현재 node의 내용물을 읽어야 다음 읽어야 할 내용물이 있는 node 파악이 가능하기 때문. 그러면 MLP를 활용하지 못한다고 볼 수 있다.
흔히 이를 해결하기 위해 각 leaf가 여러개의 key를 가질 수 있도록 유도하도록 prefix를 truncate하는 것이라고 하나, 이러면 insertion이 복잡해진다고 한다. 왜냐하면 leaf가 여러개의 key를 가지는 것이 range query에서 실제로 의미가 있도록 key들을 이동시켜가지고 leaf당 key 개수가 균형이 잘 맞춰지도록 유도해야 해서 그렇다고…
그래서 여기서는 이를 구현 안함. 대신, leaf node를 구하면 그것에 대해 작업을 하고 그 다음 leaf에 대한 내용물을 또 찾는 것이 일반적으로 흔하다는 것에 착안해서 leaf를 반환할 때마다 그 leaf에 있는 next locator과 대응되는 leaf를 prefetching한다고 한다. DB에서의 작업이 memory latency보다 보통 길기 때문에 효과적이라고 한다.
subtree-max
locator도 업데이트가 되어야 한다. 이는 위로 올라가면서 갱신이 안될때까지 갱신하면 되지 않나 싶다. (코드를 봐야 할듯)EntriesFor
에서 이루어지나 pseudocode에서는 생략했따고 한다.compare-and-swap
atomic operation을 통해 확인한다.FindChild
부분이 이 부분을 나타냄.k1
이라는 key 내용물도 읽고 다음 key를 next locator로 파악하려고 한다 해보자. 이 때 locator이 갑자기 internal node가 되었거나 삭제되었을 수 있다.k1
을 다시 읽어서 같은 version인지 확인하는 형태로 검증을 한다.k1
에 대한 search를 root부터 다시 시작하고 거기서의 next locator을 참고하는 형태로 진행.Search
는 특정 key에 대응되는 leaf node를 찾는다.depth
는 jump node 탐색 중에 활용이 된다. 자세한건 4.3 및 Algorithm 2 설명 참고.Findchild
가 핵심. i번째 (i=0~#k-1) 까지 포함한 prefix 관련 node에서 i+1번째까지 포함한 prefix 관련 node로 넘어가려고 할 때 활용.null
을 반환.ki
로 표기했다… ki+1
이 아니라. 헷갈리니 참고.regular node의 경우 현 node에서 마지막이 ki인 leaf가 존재하는지를 children bitmap을 기반으로 판단한다. 없는 node에 대해서 탐색 과정이 이루어지지 않도록 방지하기 위함. 없으면 null을 반환. 이 node가 전체 key랑 관련된 leaf라 볼 수 있기 때문.
만약 존재하는게 확인되었으면, 4.2 부문을 그대로 수행하는 SearchByParent
를 수행, 다음 prefix랑 대응되는 node entry가 위치하는 bucket 및 위치를 구해서 반환한다.
jump node의 경우는 4.3에서 얘기한 것을 활용한다. else
문에 해당. 먼저 depth가 jump node에서 보관하는 symbol의 size보다 아직 작으면 계속해서 이 FidnChild를 갱신된 depth에 대해 호출하도록 유도한다.
매 호출마다 depth를 기반으로 jump node 보관 symbol sequence의 depth번째 symbol이, ki
랑 같은지를 확인. 같지 않으면 바로 null과 초기화된 depth를 반환, 같은 경우라면 아직 탐색이 완료되지 않은 경우 depth를 계속 increment한다.
그렇게 increment된 depth가 결국 연속된 호출을 통해 node에서 보관하는 key size와 같아지면 jump node에서의 탐색이 완료되었음을 의미한다. 그러면 SearchByColor
이라는 함수를 수행하게 된다.
사실 SearchByColor
의 개념적 역할은 SearchByParent
와 다를게 없다. 해당 jump node 하위의 child중 우리의 prefix랑 관련된 node를 찾는 역할이다.
그럼 왜 별개의 함수를 만들었는가? child랑 현재 key의 parent가 같은지를 확인하려면 위에서 언급했듯 peelable hash function의 특징을 활용해 서로의 parent의 hash value가 같은지가 우선 확인이 필요하기 때문이다. 그 다음에 color 비교도 하고. 문제는 우리는 key를 아예 저장 안하고 마지막 symbol만 저장하며, jump node는 앞에 여러개의 symbol들을 포함하기 때문에 서로의 parent 간의 hash value 비교를 현재 가지고 있는 메타데이터만으로 판별하는 것이 불가능하다. 2개 이상의 끝부분 symbol들이 관여해서 그렇다.
그러면 어떻게 특정 entry가 우리가 탐색에 사용하는 prefix랑 대응되는 entry임을 파악하는가? 구조적으로 jump node는 child를 1개만 가질 수가 있기 때문에, jump node의 child에 대응되는 color을 jump node에서 보관한다. 이후 entry 비교시에 last_symbol
을 비교하는것과 더불어 이 jump child color이 entry의 color
과 같은지를 확인하면 판별이 가능하다.