Skip to content

Add a cheap hash to Expr nodes to speed up IREquality - #9432

Open
abadams wants to merge 5 commits into
mainfrom
abadams/expr_hash
Open

Add a cheap hash to Expr nodes to speed up IREquality#9432
abadams wants to merge 5 commits into
mainfrom
abadams/expr_hash

Conversation

@abadams

Copy link
Copy Markdown
Member

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 type
itself keeps the low 8 bits), so this doesn't increase the memory
footprint of an IR node.

  • Every Expr node's make() method fills in the hash from an
    ultra-simple multiply-add combination of the hashes/values of its
    arguments (combine_hash in Expr.h). Stmt nodes leave the extra
    bits zero, since equal/less_than on Stmts are much less
    frequently called than on Exprs.
  • Type::hash() (a 4-byte memcpy of the type's code/bits/lanes) lets
    type-bearing nodes like Cast fold their target type into the hash,
    so a Cast doesn't collide with its own child when they share a
    node type.
  • (U)IntImm's hash multiplies the 64-bit value by a large odd
    constant 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's equal/graph_equal/less_than/graph_less_than
    now 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_than the resulting order is arbitrary
    anyway (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, comparing
this branch against main (a3690b3b6), 4 runs per app, means shown:

appbefore (ms)after (ms)delta
bilateral_grid34.7932.60-6.3%
camera_pipe55.1054.17-1.7%
conv_layer13.5112.68-6.1%
hist14.7313.77-6.5%
iir_blur12.7711.73-8.1%
interpolate131.60123.84-5.9%
max_filter17.1115.31-10.5%
nl_means56.5853.21-6.0%
stencil_chain315.65300.65-4.8%
unsharp17.5616.40-6.6%
harris18.1217.12-5.5%
bgu578.76472.17-18.4%
blur8.387.05-15.9%
depthwise_separable_conv75.4869.01-8.6%
wavelet11.5210.51-8.8%
local_laplacian150.82141.02-6.5%
lens_blur223.75206.83-7.6%

Geomean speed-up across these 17 apps: 1.086x (8.6% faster lowering).

(fft and resize were excluded from this sweep: fft's generator
needs specific size params via its own driver rather than bare -g fft
defaults, and resize needs an explicit input.type GeneratorParam;
neither is related to this change.)

Testing

correctness_ir_equality, correctness_simplify, correctness_bounds,
correctness_cse, correctness_lots_of_dimensions, and
correctness_constant_expr all pass.

🤖 Generated with Claude Code

abadamsand others added 5 commits September 6, 2026 16:32
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>
@abadams

abadams commented Sep 7, 2026

Copy link
Copy Markdown
MemberAuthor

@mcourteaux please check if this hash serves your needs in #9400, instead of having to compute one on the fly

Comment threadsrc/Expr.h
* 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a bitfield here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I want "hash" to mean the entire 32 bits.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(otherwise I'd have to manually incorporate the node_type bits into the hash in every make method)

@abadams

abadams commented Sep 7, 2026

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
MemberAuthor

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

codecovBot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.11%. Comparing base (a3690b3) to head (2b43673).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@abadams@alexreinking