Conversation
HammingBitDistance is fully scalar, paying one population count per element. For byte and sbyte spans that is one instruction per byte, which is where the loop spends its time; wider elements already amortize the population count over 2, 4 or 8 bytes. Count single-byte spans with a nibble table instead: exclusive-or a vector of each input, look up the bit counts of the low and high nibbles, and accumulate into byte lanes, reducing to a scalar every 31 vectors, before which a lane holding at most 8 per vector cannot overflow. Measured on arm64 with Vector128: 2.5x to 2.8x for byte spans of 1KB and larger. Wider element types keep the scalar loop, where the same vectorized shape measured slower than the scalar one.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-numerics |
Author
|
Follow-up on the last line of the description: I have now built the runtime locally. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TensorPrimitives.HammingBitDistanceis fully scalar:For
byte/sbytespans — which is what bitwise Hamming distance is usually computed over, since the inputs are binary fingerprints or quantized embeddings — that is one population count instruction per byte.Change
Single-byte spans now go through a vectorized path: exclusive-or a vector from each input, look up the bit counts of the low and high nibbles in a 16-entry table with
Vector128.Shuffle, and accumulate into byte lanes. A byte lane takes at most 8 per vector, so it is reduced to the running total every 31 vectors, before it can overflow. The tail stays scalar.Wider element types deliberately keep the scalar loop, and this is the part I would most like reviewed: a 4-byte element already amortizes one population count over 4 bytes and an 8-byte element over 8, and when I measured the same vectorized shape for them it was slower than the scalar loop — 0.63x for
int, 0.32x forlong. I also measured reinterpreting the spans as 64-bit words and usingBitOperations.PopCounton those, which was slower still. So the change is scoped to the case where the measurement supports it.Vector256/Vector512are not used: their byteShuffleis a full cross-lane shuffle rather than the per-128-bit-lanevpshufb, which would undo the gain on x64.Measurements
Standalone harness (I could not build the runtime on this machine), Apple M-series arm64,
Vector128, minimum of 7 rounds, ns per call:For reference, the same shape applied to wider elements (not taken in this change):
int0.63x,long0.32x.Correctness
Differential against the scalar implementation in the harness: 90,000 cases over
byte,intandlong, lengths 1–140, random values — 0 mismatches, including every length that leaves a partial vector and lengths below one vector where the new path is not taken.I have not run the
System.Numerics.Tensorstest suite locally for the reason above, so CI is the first full validation.