Uh oh!
There was an error while loading. Please reload this page.
Add comptime hashmap - #5359
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Vexu
commented
May 16, 2020
Few questions regarding the API:
|
squeek502
commented
May 16, 2020
Out of curiosity, could you run your benchmark using the previous Keyword lookup implementation, but with the hashing removed (i.e. just doing |
Vexu
commented
May 16, 2020
Sure, there would have to be a lot of similar keys for the hash version to be faster than the plain comparison so it was not really appropriate here. The differences are however so small that I value the simplicity this provides more than the speed. |
squeek502
commented
May 16, 2020
Cool, thanks for satisfying my curiosity. 👍 |
daurnimator
commented
May 17, 2020
yes.... but how would/could that work with current comptime semantics? |
See also #3863: a comptime hashmap (indexable at runtime) is able to use perfect hashes. |
frmdstryr
commented
May 17, 2020
A more generic solution would be to just have a comptime allocator #1291. |
Re #3863: I wrote a working version of a comptime minimal perfect hashing algorithm based on this Go implementation a while back but stopped after running into #4055 while testing a perfect hashing (I've also been meaning to put the mph code in a public repo for a while but never got around to it; will try to do that and link to it in #3863) Anyway, I've tried it with the Zig tokenizer keyword lookup here to get an idea of what the difference might be if perfect hashing was used. Using release builds of the current master branch and the modified version that uses perfect hashing: $ hyperfine --min-runs 50 './build/zig-master fmt tmp/lib/std''./build/zig-perfect fmt tmp/lib/std'Benchmark #1: ./build/zig-master fmt tmp/lib/std Time (mean ± σ): 657.4 ms ± 3.0 ms [User: 595.0 ms, System: 61.0 ms] Range (min … max): 652.5 ms … 671.9 ms 50 runsBenchmark #2: ./build/zig-perfect fmt tmp/lib/std Time (mean ± σ): 638.0 ms ± 2.7 ms [User: 574.9 ms, System: 61.9 ms] Range (min … max): 633.5 ms … 646.7 ms 50 runsSummary './build/zig-perfect fmt tmp/lib/std' ran 1.03 ± 0.01 times faster than './build/zig-master fmt tmp/lib/std'So seemingly no real performance gain to be had from perfect hashing here, as the gain is the same as this comptime hashmap (although there is potential for caveats if my perfect hashing implementation is bad, which is certainly possible). EDIT: Actually adding this branch into the comparison, my perfect hashing actually performs worse: $ hyperfine --min-runs 50 './build/zig-master fmt tmp/lib/std''./build/zig-vexu fmt tmp/lib/std''./build/zig-perfect fmt tmp/lib/std'Benchmark #1: ./build/zig-master fmt tmp/lib/std Time (mean ± σ): 656.4 ms ± 2.4 ms [User: 594.8 ms, System: 59.9 ms] Range (min … max): 653.3 ms … 666.8 ms 50 runsBenchmark #2: ./build/zig-vexu fmt tmp/lib/std Time (mean ± σ): 624.5 ms ± 2.2 ms [User: 565.3 ms, System: 57.8 ms] Range (min … max): 620.6 ms … 632.3 ms 50 runsBenchmark #3: ./build/zig-perfect fmt tmp/lib/std Time (mean ± σ): 637.0 ms ± 2.0 ms [User: 579.3 ms, System: 56.4 ms] Range (min … max): 633.9 ms … 643.1 ms 50 runsSummary './build/zig-vexu fmt tmp/lib/std' ran 1.02 ± 0.00 times faster than './build/zig-perfect fmt tmp/lib/std' 1.05 ± 0.01 times faster than './build/zig-master fmt tmp/lib/std'EDIT#2: In thinking about it more, the mph performing worse makes sense. In the ComptimeHashMap implementation, lookup should be as fast as a normal HashMap, but it will waste some memory storing all the unused indexes. A MPH implementation will take up less memory, but will always have to do some amount of extra work on the lookup in order to allow that compression. In my implementation, the fnindex(str: []constu8) usize {
consthash: u64=Wyhash.hash(0, str);
consti: u64=hash & (num_buckets-1);
constseed: i32=seeds[i];
if (seed<0) {
returnoriginal_indexes[@intCast(usize, -seed)-1];
}
constfinal_i=xorshiftMult64(@intCast(usize, seed) +hash) & (num_buckets-1);
returnoriginal_indexes[final_i];
}
fnindexChecked(str: []constu8) ?usize {
consti=index(str);
returnif (std.mem.eql(u8, str, strs[i])) ielsenull;
}where fnxorshiftMult64(x_: u64) u64 {
varx=x_;
x^=x>>12;
x^=x<<25;
x^=x>>27;
returnx*%2685821657736338717;
}So, for small sets like keyword lookup, this tradeoff (size of the computed map vs lookup speed) is mostly irrelevant (size of computed map won't be too big to matter). It'd become more relevant when creating a ComptimeHashMap for something like all English words in the dictionary or something, though. |
| const math = std.math; | ||
| /// A comptime hashmap constructed with automatically constructed hash and eql functions. | ||
| pub fn AutoComptimeHashMap(comptime K: type, comptime V: type, comptime values: var) type { |
There was a problem hiding this comment.
Would ComptimeAutoHashMap maybe be a better name?
daurnimator
commented
May 20, 2020
Will this work with keys of type |
EDIT: I guess you could also make it use |
andrewrk
left a comment
There was a problem hiding this comment.
The changes to the tokenizer and translate-c look really nice!
I think this can be merged as is, but here's an idea for the future: given that we have all the values up front, it would make sense to not have to provide a hash function, since the hash function could be derived from the values. We could probably make it a lot faster and save memory too, because we don't need the 60% full handicap if we have a better hashing strategy.
For example, we can look at all the values and come up with a hash function, and then test it on all the values, and find out if there are any collisions. If there are, we can try one of two things:
- re-roll the hash function, see if a different RNG seed can give us better luck (fewer collisions)
- increase the list size
Iterate until the number of collisions is within some upper bound, and then we're done!
It would be fun to explore this, but I'm guessing we will probably want to wait until we have self-hosted readily available because I could definitely see this blowing up memory usage and compilation times in the stage1 compiler.
Anyway, feel free to merge when you feel it is ready @Vexu, it looks like a nice change.
Uh oh!
There was an error while loading. Please reload this page.
andrewrk
commented
May 26, 2020
It would be interesting to make an API specifically for small lists of strings like this, where it would compute the minimal function needed for unique index lookup. For example: conststrings= .{
"one",
"two",
"three",
"four",
"five",
"X",
};
// sort by lengthconststrings= .{
"X",
"one",
"two",
"four",
"five",
"three",
};
// generate this functionfnparse(input: []constu8) ?Tag {
switch (input.len) {
1=>returncheck("X", .X),
3=>switch (input[0]) {
'o'=>returncheck("one", .one),
't'=>returncheck("two", .two),
else=>returnnull,
},
4=>switch (input[1]) {
'o'=>returncheck("four", .four),
'i'=>returncheck("five", .five),
else=>returnnull,
},
5=>returncheck("three", .three),
else=>returnnull,
}
}
fncheck(input: []constu8, expected: []constu8, tag: Tag) ?Tag {
if (mem.eql(u8, input, expected)) {
returntag;
} else {
returnnull;
}
}I suspect this would have improved performance characteristics, specifically because it quickly finds the cases where an identifier is not a keyword, which is really common in zig code. Many identifiers will have a |
andrewrk
commented
May 26, 2020
OK I think this turned out to be a pretty good idea, see #5442 |
| var key: K = kv[0]; | ||
| var value: V = kv[1]; |
There was a problem hiding this comment.
From messing around with making the implementation in #5442 re-usable (#5442 (comment)), I found that using [0] and [1] here restricts kv to being an anonymous list literal (or an array, presumably, but that would require K and V to be the same type). This limits the use cases since (AFAIK) it's not possible to generate arrays/slices of anonymous list literals at compile time, which would be needed for using ComptimeHashMap in something like a std.meta.stringToEnum implementation.
Using kv.@"0" and kv.@"1" might be better (since that will work with both anonymous list literals and struct types with fields specifically named @"0" and @"1"), or maybe make it conditional on std.meta.trait.isIndexable if arrays are intended to be supported as well?
Vexu
commented
Jun 4, 2020
I can't think of any non string related use case for this. If someone does eventually come up with one then this should be easy to get working again. |
squeek502
commented
Jun 4, 2020
I think it's still worthwhile. One use case I've come across is creating a |
Vexu
commented
Jun 4, 2020
Enums are just fancy integers so it would be faster and likely simpler to just use an array or a switch. Only situation where this could be nice would be if the keys were structs but I can't come up with any case where the structs would be comptime known. |
I'd like to use this in a situation where I need a sparse 2d array with "groups". My exact use-case is I'm trying to represent Puyo-Puyo dropsets: In the base game, there's never more than 4 colors in a 2x2 area in one drop. However, I felt like // null for empty, 0 for color group 0, 1 for color group 1, etcpubconstDrop= [4]?u2;
pubconstDropSet= [16]Drop;was a bad way to represent was done like so: .i= [4]?u2{ 0, null, 1, null }which additionally won't scale well past 4 elements. Commonly, to represent groups like this, you combine a .i=ComptimeGrouping(
[2]u8,
.{
.{ [2]u8{ 0, 0 } },
.{ [2]u8{ 0, 1 } }
}
)or the vertical as: .lv=ComptimeGrouping(
[2]u8,
.{
.{ [2]u8{ 0, 0 }, [2]u8{ 0, 1 } },
.{ [2]u8{ 1, 0 } }
}
)and then am able to index via I understand that |
Vexu
commented
Jun 27, 2020
I doubt this is the best solution for your use case, but I guess could be a good enough reason to add this at least until the std lib audit #1629. |
@sorenbug I extracted this into its own repo. // build.zigyour_exe.addPackagePath("chm", "path/to/comptime_hash_map.zig");
// in your project@import("chm").AutoComptimeHashMap(Type, .{values});
// or just@import("path/to/comptime_hash_map.zig").AutoComptimeHashMap(Type, .{values}); |



It's a hashmap constructed at compile time that can be used as a lookup table.
As a small benchmark here is
zig fmt lib/std