map: fast_string_eq and improved comments
* improved comments and fast_string_eq * make it pass CI * enumerate traits * Add parameter back * remove space * remove parameter * Allow bootstrap compilation in one step with old vc (add new_map/2 shim).pull/4369/head^2
parent
79dad0bca9
commit
e247690fe1
|
@ -8,55 +8,59 @@ import (
|
|||
hash.wyhash
|
||||
)
|
||||
|
||||
fn C.strcmp(byteptr, byteptr) int
|
||||
fn C.memcmp(byteptr, byteptr, int) int
|
||||
|
||||
/*
|
||||
This is a very fast hashmap implementation. It has several properties that in
|
||||
combination makes it very fast. Here is a short explanation of each property.
|
||||
After reading this you should have a basic understanding of how it works:
|
||||
This is a highly optimized hashmap implementation. It has several traits that
|
||||
in combination makes it very fast and memory efficient. Here is a short expl-
|
||||
anation of each trait. After reading this you should have a basic understand-
|
||||
ing of how it functions:
|
||||
|
||||
1. |Hash-function (Wyhash)|. Wyhash is the fastest hash-function passing SMHash-
|
||||
er, so it was an easy choice.
|
||||
1. Hash-function: Wyhash. Wyhash is the fastest hash-function for short keys
|
||||
passing SMHasher, so it was an obvious choice.
|
||||
|
||||
2. |Open addressing (Robin Hood Hashing)|. With this method, a hash collision is
|
||||
resolved by probing. As opposed to linear probing, Robin Hood hashing has a sim-
|
||||
ple but clever twist: As new keys are inserted, old keys are shifted around in a
|
||||
way such that all keys stay reasonably close to the slot they originally hash to.
|
||||
2. Open addressing: Robin Hood Hashing. With this method, a hash-collision is
|
||||
resolved by probing. As opposed to linear probing, Robin Hood hashing has a
|
||||
simple but clever twist: As new keys are inserted, old keys are shifted arou-
|
||||
nd in a way such that all keys stay reasonably close to the slot they origin-
|
||||
ally hash to. A new key may displace a key already inserted if its probe cou-
|
||||
nt is larger than that of the key at the current position.
|
||||
|
||||
3. |Memory layout|. Key-value pairs are stored in a `DenseArray`, with an avera-
|
||||
ge of roughly 6.25% unused memory, as opposed to most other dynamic array imple-
|
||||
mentations with a growth factor of 1.5 or 2. The key-values keep their index in
|
||||
the array - they are not probed. Instead, this implementation uses another array
|
||||
"metas" storing "meta"s (meta-data). Each Key-value has a corresponding meta. A
|
||||
meta stores a reference to its key-value, and its index in "metas" is determined
|
||||
by the hash of the key and probing. A meta also stores bits from the hash (for
|
||||
faster rehashing etc.) and how far away it is from the index it was originally
|
||||
hashed to (probe_count). probe_count is 0 if empty, 1 if not probed, 2 if probed
|
||||
by 1, etc..
|
||||
3. Memory layout: key-value pairs are stored in a `DenseArray`. This is a dy-
|
||||
namic array with a very low volume of unused memory, at the cost of more rea-
|
||||
llocations when inserting elements. It also preserves the order of the key-v-
|
||||
alues. This array is named `key_values`. Instead of probing a new key-value,
|
||||
this map probes two 32-bit numbers collectively. The first number has its 8
|
||||
most significant bits reserved for the probe-count and the remaining 24 bits
|
||||
are cached bits from the hash which are utilized for faster re-hashing. This
|
||||
number is often referred to as `meta`. The other 32-bit number is the index
|
||||
at which the key-value was pushed to in `key_values`. Both of these numbers
|
||||
are stored in a sparse array `metas`. The `meta`s and `kv_index`s are stored
|
||||
at even and odd indices, respectively:
|
||||
|
||||
meta (64 bit) = kv_index (32 bit) | probe_count (8 bits) | hashbits (24 bits)
|
||||
metas = [meta, 0, meta, 0, meta, meta, meta, 0, ...]
|
||||
key_values = [kv, kv, kv, kv, kv, ...]
|
||||
metas = [meta, kv_index, 0, 0, meta, kv_index, 0, 0, meta, kv_index, ...]
|
||||
key_values = [kv, kv, kv, ...]
|
||||
|
||||
4. |Power of two size array|. The size of metas is a power of two. This makes it
|
||||
possible to find a bucket from a hash code by using "hash & (SIZE -1)" instead
|
||||
of "abs(hash) % SIZE". Modulo is extremely expensive so using '&' is a big perf-
|
||||
ormance improvement. The general concern with this is that you only use the low-
|
||||
er bits of the hash and that can cause more collisions. This is solved by using
|
||||
good hash-function.
|
||||
4. The size of metas is a power of two. This enables the use of bitwise AND
|
||||
to convert the 64-bit hash to a bucket/index that doesn't overflow metas. If
|
||||
the size is power of two you can use "hash & (SIZE - 1)" instead of "hash %
|
||||
SIZE". Modulo is extremely expensive so using '&' is a big performance impro-
|
||||
vement. The general concern with this approach is that you only make use of
|
||||
the lower bits of the hash which can cause more collisions. This is solved by
|
||||
using a well-dispersed hash-function.
|
||||
|
||||
5. |Extra metas|. The hashmap keeps track of the highest probe_count. The trick
|
||||
is to allocate extra_metas > max(probe_count), so you never have to do any boun-
|
||||
ds-checking because the extra metas ensures that an element will never go beyond
|
||||
5. The hashmap keeps track of the highest probe_count. The trick is to alloc-
|
||||
ate `extra_metas` > max(probe_count), so you never have to do any bounds-che-
|
||||
cking since the extra meta memory ensures that a meta will never go beyond
|
||||
the last index.
|
||||
|
||||
6. |Cached rehashing|. When the load_factor of the map exceeds the max_load_fac-
|
||||
tor the size of metas is doubled and all the elements need to be "rehashed" to
|
||||
find the index in the new array. Instead of rehashing completely, it simply uses
|
||||
the hashbits stored in the meta.
|
||||
6. Cached rehashing. When the `load_factor` of the map exceeds the `max_load_
|
||||
factor` the size of metas is doubled and all the key-values are "rehashed" to
|
||||
find the index for their meta's in the new array. Instead of rehashing compl-
|
||||
etely, it simply uses the cached-hashbits stored in the meta, resulting in
|
||||
much faster rehashing.
|
||||
*/
|
||||
|
||||
|
||||
const (
|
||||
// Number of bits from the hash stored for each entry
|
||||
hashbits = 24
|
||||
|
@ -79,6 +83,17 @@ const (
|
|||
probe_inc = u32(0x01000000)
|
||||
)
|
||||
|
||||
// This function is intended to be fast when
|
||||
// the strings are very likely to be equal
|
||||
// TODO: add branch prediction hints
|
||||
[inline]
|
||||
fn fast_string_eq(a, b string) bool {
|
||||
if a.len != b.len {
|
||||
return false
|
||||
}
|
||||
return C.memcmp(a.str, b.str, b.len) == 0
|
||||
}
|
||||
|
||||
struct KeyValue {
|
||||
key string
|
||||
mut:
|
||||
|
@ -107,7 +122,7 @@ fn new_dense_array() DenseArray {
|
|||
}
|
||||
|
||||
// Push element to array and return index
|
||||
// The growth-factor is roughly 12.5 `(x + (x >> 3))`
|
||||
// The growth-factor is roughly 1.125 `(x + (x >> 3))`
|
||||
[inline]
|
||||
fn (d mut DenseArray) push(kv KeyValue) u32 {
|
||||
if d.cap == d.size {
|
||||
|
@ -142,7 +157,7 @@ pub struct map {
|
|||
// Byte size of value
|
||||
value_bytes int
|
||||
mut:
|
||||
// highest even index in the hashtable
|
||||
// highest even index in the hashtable
|
||||
cap u32
|
||||
// Number of cached hashbits left for rehasing
|
||||
cached_hashbits byte
|
||||
|
@ -151,18 +166,22 @@ mut:
|
|||
// Array storing key-values (ordered)
|
||||
key_values DenseArray
|
||||
// Pointer to meta-data:
|
||||
// Odd indices stores index in `key_values`.
|
||||
// Even indices stores probe_count and hashbits.
|
||||
// Odd indices store kv_index.
|
||||
// Even indices store probe_count and hashbits.
|
||||
metas &u32
|
||||
// Extra metas that allows for no ranging when incrementing
|
||||
// index in the hashmap
|
||||
extra_metas u32
|
||||
pub mut:
|
||||
// Number of key-values currently in the hashmap
|
||||
// Number of key-values currently in the hashmap
|
||||
size int
|
||||
}
|
||||
|
||||
// TODO: remove this after vc is regenerated.
|
||||
fn new_map(n, value_bytes int) map {
|
||||
return new_map_1(value_bytes)
|
||||
}
|
||||
fn new_map_1(value_bytes int) map {
|
||||
return map{
|
||||
value_bytes: value_bytes
|
||||
cap: init_cap
|
||||
|
@ -176,7 +195,7 @@ fn new_map(n, value_bytes int) map {
|
|||
}
|
||||
|
||||
fn new_map_init(n, value_bytes int, keys &string, values voidptr) map {
|
||||
mut out := new_map(n, value_bytes)
|
||||
mut out := new_map_1(value_bytes)
|
||||
for i in 0 .. n {
|
||||
out.set(keys[i], byteptr(values) + i * value_bytes)
|
||||
}
|
||||
|
@ -244,7 +263,7 @@ fn (m mut map) set(key string, value voidptr) {
|
|||
// While we might have a match
|
||||
for meta == m.metas[index] {
|
||||
kv_index := m.metas[index + 1]
|
||||
if C.strcmp(key.str, m.key_values.data[kv_index].key.str) == 0 {
|
||||
if fast_string_eq(key, m.key_values.data[kv_index].key) {
|
||||
C.memcpy(m.key_values.data[kv_index].value, value, m.value_bytes)
|
||||
return
|
||||
}
|
||||
|
@ -320,7 +339,7 @@ fn (m map) get3(key string, zero voidptr) voidptr {
|
|||
index,meta = m.meta_less(index, meta)
|
||||
for meta == m.metas[index] {
|
||||
kv_index := m.metas[index + 1]
|
||||
if C.strcmp(key.str, m.key_values.data[kv_index].key.str) == 0 {
|
||||
if fast_string_eq(key, m.key_values.data[kv_index].key) {
|
||||
out := malloc(m.value_bytes)
|
||||
C.memcpy(out, m.key_values.data[kv_index].value, m.value_bytes)
|
||||
return out
|
||||
|
@ -332,14 +351,11 @@ fn (m map) get3(key string, zero voidptr) voidptr {
|
|||
}
|
||||
|
||||
fn (m map) exists(key string) bool {
|
||||
if m.value_bytes == 0 {
|
||||
return false
|
||||
}
|
||||
mut index,mut meta := m.key_to_index(key)
|
||||
index,meta = m.meta_less(index, meta)
|
||||
for meta == m.metas[index] {
|
||||
kv_index := m.metas[index + 1]
|
||||
if C.strcmp(key.str, m.key_values.data[kv_index].key.str) == 0 {
|
||||
if fast_string_eq(key, m.key_values.data[kv_index].key) {
|
||||
return true
|
||||
}
|
||||
index += 2
|
||||
|
@ -354,7 +370,7 @@ pub fn (m mut map) delete(key string) {
|
|||
// Perform backwards shifting
|
||||
for meta == m.metas[index] {
|
||||
kv_index := m.metas[index + 1]
|
||||
if C.strcmp(key.str, m.key_values.data[kv_index].key.str) == 0 {
|
||||
if fast_string_eq(key, m.key_values.data[kv_index].key) {
|
||||
for (m.metas[index + 2]>>hashbits) > 1 {
|
||||
m.metas[index] = m.metas[index + 2] - probe_inc
|
||||
m.metas[index + 1] = m.metas[index + 3]
|
||||
|
@ -380,11 +396,9 @@ pub fn (m mut map) delete(key string) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: add optimization in case of no deletes
|
||||
pub fn (m &map) keys() []string {
|
||||
mut keys := [''].repeat(m.size)
|
||||
if m.value_bytes == 0 {
|
||||
return keys
|
||||
}
|
||||
mut j := 0
|
||||
for i := u32(0); i < m.key_values.size; i++ {
|
||||
if m.key_values.data[i].key.str == 0 {
|
||||
|
@ -408,10 +422,6 @@ pub fn (m map) free() {
|
|||
free(m.key_values.data)
|
||||
}
|
||||
|
||||
pub fn (m map) print() {
|
||||
println('TODO')
|
||||
}
|
||||
|
||||
pub fn (m map_string) str() string {
|
||||
if m.size == 0 {
|
||||
return '{}'
|
||||
|
|
|
@ -1127,7 +1127,7 @@ fn (g mut Gen) expr(node ast.Expr) {
|
|||
}
|
||||
g.write('})')
|
||||
} else {
|
||||
g.write('new_map(1, sizeof($value_typ_str))')
|
||||
g.write('new_map_1(sizeof($value_typ_str))')
|
||||
}
|
||||
}
|
||||
ast.None {
|
||||
|
@ -2902,7 +2902,7 @@ fn (g Gen) type_default(typ table.Type) string {
|
|||
}
|
||||
if sym.kind == .map {
|
||||
value_type_str := g.typ(sym.map_info().value_type)
|
||||
return 'new_map(1, sizeof($value_type_str))'
|
||||
return 'new_map_1(sizeof($value_type_str))'
|
||||
}
|
||||
// Always set pointers to 0
|
||||
if table.type_is_ptr(typ) {
|
||||
|
|
|
@ -60,7 +60,7 @@ int main(int argc, char** argv) {
|
|||
});
|
||||
Foo af_idx_el = (*(Foo*)array_get(arr_foo, 0));
|
||||
string foo_a = af_idx_el.a;
|
||||
map_string_string m1 = new_map(1, sizeof(string));
|
||||
map_string_string m1 = new_map(sizeof(string));
|
||||
map_string_int m2 = new_map_init(2, sizeof(int), (string[2]){tos3("v"), tos3("lang"), }, (int[2]){1, 2, });
|
||||
string ma1 = tos3("hello");
|
||||
string ma2 = tos3("vlang");
|
||||
|
|
Loading…
Reference in New Issue