Uh oh!
There was an error while loading. Please reload this page.
Add a cheap hash to Expr nodes to speed up IREquality - #9432
Conversation
Each Expr node's make() method now fills in BaseExprNode::hash, an ultra-simple multiply-add combination of the node type and the hashes/values of its arguments. equal()/graph_equal() use it to short-circuit on a hash mismatch before doing a full recursive comparison, and less_than()/graph_less_than() use it directly to order nodes when hashes differ, since that ordering is arbitrary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the hash from a separate BaseExprNode::hash field into a union with IRNode::node_type: the low 8 bits are the node type (as before) and the upper 24 bits are the hash, so this doesn't grow IRNode (the comment already noted these bits were free padding). Stmt nodes leave the upper bits zero. IRNode::set_hash keeps the low byte's node type intact while masking in a newly-computed 32-bit hash's upper 24 bits (its low bits are of poor quality due to the multiply-add construction, so they're discarded rather than shifted into the result). Add Type::hash() (a memcpy of its first 4 bytes) so type-bearing nodes like Cast can fold their type into the hash instead of just passing their child's hash through unchanged, which would otherwise collide with equal-typed child nodes of the same kind. IREquality.h now compares IRNode::hash directly instead of node_type followed by a separate BaseExprNode hash check, since a hash mismatch already implies a node_type mismatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous big-endian branch kept args_hash's low 24 bits (the low-quality end of a multiply-add hash) instead of discarding them. Shift right by 8 first to keep the high-quality high bits, matching what the little-endian branch already does by masking. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
set_hash keeps only the high 24 bits of its argument. The previous IntImm/UIntImm hash sliced the 64-bit value into two 32-bit halves and combined them, which put all the entropy of small values (the common case) in the low bits that set_hash then throws away, making every small IntImm/UIntImm of a given sign collide. Multiply the value by a large odd 64-bit constant and keep the high 32 bits of the product instead (Knuth multiplicative hashing), which mixes the low bits of the value into the high bits of the result even when the value itself is small. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mcourteaux please check if this hash serves your needs in #9400, instead of having to compute one on the fly |
| * 32 free bits in between the ref count and the Type field anyway. We use | ||
| * the first 8 to store the node type, and the next 24 as a hash of the | ||
| * children of the node, to make syntactic comparisons faster. */ | ||
| union { |
There was a problem hiding this comment.
because I want "hash" to mean the entire 32 bits.
There was a problem hiding this comment.
(otherwise I'd have to manually incorporate the node_type bits into the hash in every make method)
If I use this to swap our expr-keyed maps and sets to be unordered_maps and unordered_sets, lowering time gets worse, and if I use ankerl::unordered_dense set and maps, lowering time doesn't change. So for this PR I'll leave this hash as something operator< uses for a regular map or set. |
abadams
commented
Sep 7, 2026
A custom map/set optimized for the sizes we see in practice was only 1.5% faster than this branch, so not worth the code complexity. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@## main #9432 +/- ##
==========================================
- Coverage 70.12% 70.11% -0.01%
==========================================
Files 261 261 Lines 79405 79462 +57 Branches 19362 19365 +3 ==========================================
+ Hits 55684 55717 +33 + Misses 17896 17891 -5 - Partials 5825 5854 +29 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
Adds a 32-bit hash to every IR node, packed into the previously-unused
upper 24 bits of
IRNode::node_type's storage word (the node typeitself keeps the low 8 bits), so this doesn't increase the memory
footprint of an IR node.
Exprnode'smake()method fills in the hash from anultra-simple multiply-add combination of the hashes/values of its
arguments (
combine_hashinExpr.h).Stmtnodes leave the extrabits zero, since
equal/less_thanon Stmts are much lessfrequently called than on Exprs.
Type::hash()(a 4-bytememcpyof the type's code/bits/lanes) letstype-bearing nodes like
Castfold their target type into the hash,so a
Castdoesn't collide with its own child when they share anode type.
(U)IntImm's hash multiplies the 64-bit value by a large oddconstant and keeps the high 32 bits (Knuth multiplicative hashing),
so small immediates (by far the most common case) still get a well
distributed hash instead of colliding with each other.
IREquality.h'sequal/graph_equal/less_than/graph_less_thannow compare/order by this hash before falling back to a full
recursive comparison. A hash mismatch already implies the node
types differ, so the separate node-type check was dropped. For
less_than/graph_less_thanthe resulting order is arbitraryanyway (only used for map keys), so it's fine to order directly by
hash when hashes differ.
Performance
Measured with
HL_TIME_LOWERING_PASSES=1,target=host, comparingthis branch against
main(a3690b3b6), 4 runs per app, means shown:Geomean speed-up across these 17 apps: 1.086x (8.6% faster lowering).
(
fftandresizewere excluded from this sweep:fft's generatorneeds specific size params via its own driver rather than bare
-g fftdefaults, and
resizeneeds an explicitinput.typeGeneratorParam;neither is related to this change.)
Testing
correctness_ir_equality,correctness_simplify,correctness_bounds,correctness_cse,correctness_lots_of_dimensions, andcorrectness_constant_exprall pass.🤖 Generated with Claude Code