-
Notifications
You must be signed in to change notification settings - Fork 473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Fast isValidCell #496
base: master
Are you sure you want to change the base?
Conversation
src/h3lib/lib/h3Index.c
Outdated
// The 4 mode bits should be 0b0001 (H3_CELL_MODE) | ||
// The 3 reserved bits should be 0b000 | ||
// In total, the top 8 bits should be 0b00001000 | ||
if (GT(h, 8) != 0b00001000) return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Binary 0b
literals might not be portable, in which case we can always switch to hex. Binary is a bit easier for prototyping, tho :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative implementation:
(also, could use #define
s for the bit lengths here)
if (GT(h, 1) != 0) return 0;
h <<= 1;
if (GT(h, 4) != 1) return 0;
h <<= 4;
if (GT(h, 3) != 0) return 0;
h <<= 3;
This alternative might be clearer, and Clang is even able to compile each version to the same instructions. However, GCC doesn't seem to produce the same for the longer version: https://godbolt.org/z/sjKed8jn1
(Even though the last shift in each example is optimized out to a no-op, still gives you the general idea.)
// The first nonzero digit can't be a 1 (i.e., "deleted subsequence", | ||
// PENTAGON_SKIPPED_DIGIT, or K_AXES_DIGIT). | ||
// Test for pentagon base cell first to avoid this loop if possible. | ||
if (isBaseCellPentagonArr[bc]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using this lookup table seems to be a bit faster than calling _isBaseCellPentagon(bc)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative:
int _isBaseCellPentagon(int baseCell) {
switch(baseCell) {
case 4:
case 14:
case 24:
case 38:
case 49:
case 58:
case 63:
case 72:
case 83:
case 97:
case 107:
case 117:
return 1;
default:
return 0;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can use baseCellData[bc].isPentagon
that should be equivalent? Not sure of the linking cost of going outside the file.
src/h3lib/lib/h3Index.c
Outdated
// Now check that all the unused digits after `res` are | ||
// set to 7 (INVALID_DIGIT). | ||
// Bit shift operations allow us to avoid looping through digits; | ||
// this saves time in benchmarks. | ||
int shift = (15 - res) * 3; | ||
uint64_t m = 0; | ||
m = ~m; | ||
m >>= shift; | ||
m = ~m; | ||
if (h != m) return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatives, but the one above using bit shifting seemed to be the fastest:
Loop
(slowest of all 3)
for (; r <= 15; r++) {
if (GT(h, 3) != 7) return 0;
h <<= 3;
}
Lookup table
static const uint64_t m7s[16] = {
0b1111111111111111111111111111111111111111111110000000000000000000,
0b1111111111111111111111111111111111111111110000000000000000000000,
0b1111111111111111111111111111111111111110000000000000000000000000,
0b1111111111111111111111111111111111110000000000000000000000000000,
0b1111111111111111111111111111111110000000000000000000000000000000,
0b1111111111111111111111111111110000000000000000000000000000000000,
0b1111111111111111111111111110000000000000000000000000000000000000,
0b1111111111111111111111110000000000000000000000000000000000000000,
0b1111111111111111111110000000000000000000000000000000000000000000,
0b1111111111111111110000000000000000000000000000000000000000000000,
0b1111111111111110000000000000000000000000000000000000000000000000,
0b1111111111110000000000000000000000000000000000000000000000000000,
0b1111111110000000000000000000000000000000000000000000000000000000,
0b1111110000000000000000000000000000000000000000000000000000000000,
0b1110000000000000000000000000000000000000000000000000000000000000,
0b0};
if (h != m7s[res]) return 0;
Some benchmark runs. New algo seems to take roughly 65% or 53% of the time of the current, depending on the input cells. new algobuild/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1141.561000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 785.043000 microseconds per iteration (1000 iterations)
build/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1138.550000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 783.216000 microseconds per iteration (1000 iterations)
build/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1141.815000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 782.925000 microseconds per iteration (1000 iterations) current algobuild/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1788.558000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 1462.292000 microseconds per iteration (1000 iterations)
build/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1750.815000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 1502.663000 microseconds per iteration (1000 iterations)
build/bin/benchmarkIsValidCell
-- pentagonChildren_8_14: 1773.345000 microseconds per iteration (1000 iterations)
-- pentagonChildren_2_8: 1467.033000 microseconds per iteration (1000 iterations) Here are the ratios: new = [
[1141.561000, 785.043000],
[1138.550000, 783.216000],
[1141.815000, 782.925000],
]
old = [
[1788.558000, 1462.292000],
[1750.815000, 1502.663000],
[1773.345000, 1467.033000],
]
np.array(new)/np.array(old) gives array([[0.63825775, 0.53685789],
[0.65029715, 0.52121866],
[0.6438764 , 0.5336792 ]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super fun :). The only optimization I have to add here is Duff's Device, not sure if it will gain you much but it could improve the remaining loops.
// Get Top t bits from h | ||
#define GT(h, t) ((h) >> (64 - (t))) | ||
|
||
static const bool isBaseCellPentagonArr[128] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this an improvement over baseCellData[baseCellNum].isPentagon
?
src/h3lib/lib/h3Index.c
Outdated
// | ... | ... | | ||
// | Digit 15 | 3 | | ||
// | ||
// Additionally, we try to group operations and void loops when possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: avoid loops
?
// The first nonzero digit can't be a 1 (i.e., "deleted subsequence", | ||
// PENTAGON_SKIPPED_DIGIT, or K_AXES_DIGIT). | ||
// Test for pentagon base cell first to avoid this loop if possible. | ||
if (isBaseCellPentagonArr[bc]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can use baseCellData[bc].isPentagon
that should be equivalent? Not sure of the linking cost of going outside the file.
src/h3lib/lib/h3Index.c
Outdated
@@ -78,49 +78,108 @@ void H3_EXPORT(h3ToString)(H3Index h, char *str, size_t sz) { | |||
sprintf(str, "%" PRIx64, h); | |||
} | |||
|
|||
// Get Top t bits from h | |||
#define GT(h, t) ((h) >> (64 - (t))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This looks like "greater than" to me - GET_BITS
?
if (digit != INVALID_DIGIT) return 0; | ||
// After (possibly) taking care of pentagon logic, check that | ||
// the remaining digits up to `res` are not 7 (INVALID_DIGIT). | ||
// Don't see a way to avoid this loop :( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could unroll it into groups of 4 or 8, possibly with a macro to simplify the code. This would only work for hex base cells though, or for pentagon base cells if you re-check all the digits you just checked. Might be worth trying.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this PR is still alive, but I've just stumbled on it.
Last week I've implemented my own quick isValidCell
(a simpler one though, because for my use case I don't need to have any specific check for pentagons) and I think this loop can be avoided.
The search for the pattern 0b111
may not be easy to implement quickly, but on the other hand we can apply a bitwise NOT and look for 0b000
which is easier 🙂.
Indeed, looking for a null triplet is similar to looking for a nul-byte, something we do a lot in C (think strlen
).
By digging into the annals of the Old Gods, a.k.a. comp.lang.c
, we can extract this little gem: Alan Mycroft's null-byte detection algorithm, posted in 1987.
Tweaking the bitmasks to works on 3-bit instead of 8-bit does the trick and we end up with a code that would rougly look like
#include<stdint.h>
#include<stdio.h>
#include<assert.h>
#define MAX_RESOLUTION 15
#define CELL_BITSIZE 3
int has_invalid_digit(uint64_t index) {
static const uint64_t LO_MAGIC = 0x49249249249ULL; // ...001 001 001...
static const uint64_t HI_MAGIC = 0x124924924924ULL; // ...100 100 100...
const uint64_t resolution = (index >> 52 & 0xF);
const uint64_t unused_count = MAX_RESOLUTION - resolution;
const uint64_t unused_bitsize = unused_count * CELL_BITSIZE;
const uint64_t digits_mask = (1ULL << (resolution * CELL_BITSIZE)) - 1;
const uint64_t digits = (index >> unused_bitsize) & digits_mask;
return ((~digits - LO_MAGIC) & (digits & HI_MAGIC)) != 0ULL;
}
int main(void) {
// Valid H3 index
// 0-0001-000-1100-0010101-110-101-110-001-100-000-101-001-100-110-110-101-111-111-111
assert(!has_invalid_digit(0x08c2bae305336bffULL));
// First digit invalid.
// 0-0001-000-1100-0010101-111-101-110-001-100-000-101-001-100-110-110-101-111-111-111
assert(has_invalid_digit(0x08c2bee305336bffULL));
// Digit in the middle invalid.
// 0-0001-000-1100-0010101-110-101-110-001-100-111-101-001-100-110-110-101-111-111-111
assert(has_invalid_digit(0x08c2bae33d336bffULL));
// Last digit invalid.
// 0-0001-000-1100-0010101-110-101-110-001-100-000-101-001-100-110-110-111-111-111-111
assert(has_invalid_digit(0x08c2bae305336fffULL));
return 0;
}
So, in the end we can replace the loop by ((~digits - LO_MAGIC) & (digits & HI_MAGIC)) != 0ULL
.
For sure, this would deserve some nice comments, because it's not obvious on a first read (I went heavy on the comments for my implementation xD).
If something is not clear I can give a more detailed explanation (but that basically boils down on how Alan Mycroft's null-byte detection works).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, this is awesome! What a great find!
I spent a little time reworking this PR to add your logic. It definitely helps on the benchmarks.
It looks like you also came up with some improvements for the pentagon branch here: nmandery/h3ron#34
I'll have to parse that as well. Also happy if you want to make a pull against this PR!
src/h3lib/lib/h3Index.c
Outdated
if (GT(h, nBitsDigit) == 7) return 0; | ||
h <<= nBitsDigit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming bitshifts are as expensive as they were when I was in college, it may make sense to just have an array of 16 masks and do if (h & masks[r] == masks[r]) return 0;
as the only part of the inner loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran some toy timing tests and didn't see much difference between masking vs shifting: https://gist.github.com/ajfriend/32571f0af1f3ea3133b6836fc861c730
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't be surprised if both code generate the same (or almost) assembly behind.
Compiler are quite clever for this kind of simple expression nowadays, like you don't have to write n >> 1
instead of n / 2
and so on.
This is basic peephole optimisation.
todo: maybe add a benchmark that runs through all the cells in a resolution. |
@slaperche-zenly It looks like your fast version of the pentagon branch would depend on a C equivalent of Anyone have an idea if there is such a C equivalent? |
C compilers usually expose this as a builtin or intrinsic (IIRC it's |
That page is cool! I was thinking of the trick converting the int to a float and extracting the exponent, but not sure how portable that code would be. @isaacbrodsky @nrabinowitz @dfellis, thoughts? |
As far as portability goes, you should be fine as long as you have IEEE-754 compliant floating point numbers, which should be everywhere (bar a few embedded or old/exotic systems). I would be more worried about the type punning that may or may not run afoul of the strict aliasing rules (IIRC, the standard is not super clear on what’s allowed and what summon nasal daemons...) The approach based on DeBruijn sequence, adapted to 64-bit, sounds safer to me. |
g <<= 19; | ||
g >>= 19; // at this point, g < 2^45 - 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not replace this with a simple &
mask that has 0
s for the top 19 bits and 1
s for the lower 45 bits? That should be much faster than shifting up and down 19 times each.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, agreed!
// reinterpret the double bits as uint64_t | ||
g = *(uint64_t *)&f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear if this is safe - it may be necessary to use a different implementation on a platform like ARM (we can probably test this on Raspberry Pi). We should check if UBSan complains about this.
In working on a new
compactCells
implementation,isValidCell
was showing up as a bottleneck in some of the benchmarks, so I started playing around with optimizing it.This is still experimental (e.g., I'm still using
7
instead of theINVALID_DIGIT
macro just because I find that easier for prototyping).Putting this up now as a draft PR to get some early feedback, to take notes on what I've tried so far, and to discuss different implementation options.
Current benchmarks have this new implementation taking about 60% of the time of the current implementation. Note that the benchmarks can depend on the resolution of the cells; low resolution cells tend to go faster because the last
INVALID_DIGIT
check can avoid more looping (I think...).