From ee827e92cd8c28528147d95706877694eadf5158 Mon Sep 17 00:00:00 2001 From: Andrew Adams Date: Sun, 6 Sep 2026 16:32:01 -0700 Subject: [PATCH 1/5] Add a cheap hash to Expr nodes for fast IREquality pre-checks 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 --- src/Expr.cpp | 6 ++++++ src/Expr.h | 29 +++++++++++++++++++++++++++++ src/IR.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ src/IREquality.h | 26 ++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/src/Expr.cpp b/src/Expr.cpp index 7d55fe9350c4..fd59f14a45fa 100644 --- a/src/Expr.cpp +++ b/src/Expr.cpp @@ -1,3 +1,5 @@ +#include + #include "Expr.h" #include "IROperator.h" // for lossless_cast() @@ -35,6 +37,7 @@ const IntImm *IntImm::make(Type t, int64_t value) { IntImm *node = new IntImm; node->type = t; node->value = value; + node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)value); return node; } @@ -51,6 +54,7 @@ const UIntImm *UIntImm::make(Type t, uint64_t value) { UIntImm *node = new UIntImm; node->type = t; node->value = value; + node->hash = combine_hash((uint64_t)node->node_type, value); return node; } @@ -77,6 +81,7 @@ const FloatImm *FloatImm::make(Type t, double value) { internal_error << "FloatImm must be 16, 32, or 64-bit\n"; } + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(node->value)); return node; } @@ -84,6 +89,7 @@ const StringImm *StringImm::make(const std::string &val) { StringImm *node = new StringImm; node->type = type_of(); node->value = val; + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(val)); return node; } diff --git a/src/Expr.h b/src/Expr.h index 5a800e7bd625..046bcbe34cdf 100644 --- a/src/Expr.h +++ b/src/Expr.h @@ -161,8 +161,31 @@ struct BaseExprNode : public IRNode { } virtual Expr mutate_expr(IRMutator *v) const = 0; Type type; + + /** A cheap hash of the node, filled in by the make() method of each + * node from the node type and the hashes/values of its arguments. Not a + * high-quality hash (e.g. it ignores the identity of any Buffer/Parameter + * arguments), but it's cheap enough that it can be used as a fast + * pre-check in IREquality.h before doing a full IR comparison, and as a + * hash table key elsewhere, so long as some hash collisions are tolerated. */ + uint64_t hash = 0; }; +/** Combine one or more child hashes (or plain uint64_t fields) into a + * running hash, for use in the make() methods of Expr nodes below when + * setting BaseExprNode::hash. */ +// @{ +HALIDE_ALWAYS_INLINE +uint64_t combine_hash(uint64_t hash, uint64_t child_hash) { + return hash * 6364136223846793005ULL + child_hash; +} + +template +HALIDE_ALWAYS_INLINE uint64_t combine_hash(uint64_t hash, uint64_t child_hash, Rest... rest) { + return combine_hash(combine_hash(hash, child_hash), rest...); +} +// @} + /** We use the "curiously recurring template pattern" to avoid duplicated code in the IR Nodes. These classes live between the abstract base classes and the actual IR Nodes in the @@ -342,6 +365,12 @@ struct Expr : public Internal::IRHandle { Type type() const { return get()->type; } + + /** Get the cheap hash of this expression node. See BaseExprNode::hash. */ + HALIDE_ALWAYS_INLINE + uint64_t hash() const { + return get()->hash; + } }; /** This lets you use an Expr as a key in a map of the form diff --git a/src/IR.cpp b/src/IR.cpp index a5ff626ed7e6..bb092bd6f9b1 100644 --- a/src/IR.cpp +++ b/src/IR.cpp @@ -4,6 +4,7 @@ #include "IROperator.h" #include "IRPrinter.h" #include "IRVisitor.h" +#include #include #include @@ -44,6 +45,7 @@ Expr Cast::make(Type t, Expr v) { Cast *node = new Cast; node->type = t; + node->hash = combine_hash((uint64_t)node->node_type, v.hash()); node->value = std::move(v); return node; } @@ -60,6 +62,7 @@ Expr Reinterpret::make(Type t, Expr v) { Reinterpret *node = new Reinterpret; node->type = t; + node->hash = combine_hash((uint64_t)node->node_type, v.hash()); node->value = std::move(v); return node; } @@ -71,6 +74,7 @@ Expr Add::make(Expr a, Expr b) { Add *node = new Add; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -83,6 +87,7 @@ Expr Sub::make(Expr a, Expr b) { Sub *node = new Sub; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -95,6 +100,7 @@ Expr Mul::make(Expr a, Expr b) { Mul *node = new Mul; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -107,6 +113,7 @@ Expr Div::make(Expr a, Expr b) { Div *node = new Div; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -119,6 +126,7 @@ Expr Mod::make(Expr a, Expr b) { Mod *node = new Mod; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -131,6 +139,7 @@ Expr Min::make(Expr a, Expr b) { Min *node = new Min; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -143,6 +152,7 @@ Expr Max::make(Expr a, Expr b) { Max *node = new Max; node->type = a.type(); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -155,6 +165,7 @@ Expr EQ::make(Expr a, Expr b) { EQ *node = new EQ; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -167,6 +178,7 @@ Expr NE::make(Expr a, Expr b) { NE *node = new NE; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -179,6 +191,7 @@ Expr LT::make(Expr a, Expr b) { LT *node = new LT; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -191,6 +204,7 @@ Expr LE::make(Expr a, Expr b) { LE *node = new LE; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -203,6 +217,7 @@ Expr GT::make(Expr a, Expr b) { GT *node = new GT; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -215,6 +230,7 @@ Expr GE::make(Expr a, Expr b) { GE *node = new GE; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -229,6 +245,7 @@ Expr And::make(Expr a, Expr b) { And *node = new And; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -243,6 +260,7 @@ Expr Or::make(Expr a, Expr b) { Or *node = new Or; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); node->a = std::move(a); node->b = std::move(b); return node; @@ -254,6 +272,7 @@ Expr Not::make(Expr a) { Not *node = new Not; node->type = Bool(a.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, a.hash()); node->a = std::move(a); return node; } @@ -269,6 +288,8 @@ Expr Select::make(Expr condition, Expr true_value, Expr false_value) { Select *node = new Select; node->type = true_value.type(); + node->hash = combine_hash((uint64_t)node->node_type, condition.hash(), + true_value.hash(), false_value.hash()); node->condition = std::move(condition); node->true_value = std::move(true_value); node->false_value = std::move(false_value); @@ -285,6 +306,8 @@ Expr Load::make(Type type, const std::string &name, Expr index, Buffer<> image, Load *node = new Load; node->type = type; node->name = name; + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), + index.hash(), predicate.hash()); node->predicate = std::move(predicate); node->index = std::move(index); node->image = std::move(image); @@ -320,6 +343,8 @@ Expr Ramp::make(Expr base, Expr stride, int lanes) { Ramp *node = new Ramp; node->type = base.type().with_lanes(lanes * base.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)lanes, + base.hash(), stride.hash()); node->base = std::move(base); node->stride = std::move(stride); node->lanes = lanes; @@ -332,6 +357,7 @@ Expr Broadcast::make(Expr value, int lanes) { Broadcast *node = new Broadcast; node->type = value.type().with_lanes(lanes * value.type().lanes()); + node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)lanes, value.hash()); node->value = std::move(value); node->lanes = lanes; return node; @@ -344,6 +370,8 @@ Expr Let::make(const std::string &name, Expr value, Expr body) { Let *node = new Let; node->type = body.type(); node->name = name; + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), + value.hash(), body.hash()); node->value = std::move(value); node->body = std::move(body); return node; @@ -974,6 +1002,11 @@ Expr Call::make(Type type, const std::string &name, const std::vector &arg Call *node = new Call; node->type = type; node->name = name; + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), + (uint64_t)call_type, (uint64_t)value_index); + for (const auto &arg : args) { + node->hash = combine_hash(node->hash, arg.hash()); + } node->args = args; node->call_type = call_type; node->func = std::move(func); @@ -995,6 +1028,7 @@ Expr Variable::make(Type type, const std::string &name, Buffer<> image, Paramete Variable *node = new Variable; node->type = type; node->name = name; + node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name)); node->image = std::move(image); node->param = std::move(param); node->reduction_domain = std::move(reduction_domain); @@ -1017,6 +1051,13 @@ Expr Shuffle::make(const std::vector &vectors, Shuffle *node = new Shuffle; node->type = element_ty.with_lanes((int)indices.size()); + node->hash = (uint64_t)node->node_type; + for (int i : indices) { + node->hash = combine_hash(node->hash, (uint64_t)i); + } + for (const auto &v : vectors) { + node->hash = combine_hash(node->hash, v.hash()); + } node->vectors = vectors; node->indices = indices; return node; @@ -1259,6 +1300,7 @@ Expr VectorReduce::make(VectorReduce::Operator op, << lanes << " " << vec.type().lanes() << "\n"; VectorReduce *node = new VectorReduce; node->type = vec.type().with_lanes(lanes); + node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)op, vec.hash()); node->op = op; node->value = std::move(vec); return node; diff --git a/src/IREquality.h b/src/IREquality.h index c6987f873c4e..d273f0150f9a 100644 --- a/src/IREquality.h +++ b/src/IREquality.h @@ -40,6 +40,12 @@ bool equal(const IRNode &a, const IRNode &b) { return true; } else if (a.node_type != b.node_type) { return false; + } else if (a.node_type <= StrongestExprNodeType && + ((const BaseExprNode &)a).hash != ((const BaseExprNode &)b).hash) { + // Exprs (unlike Stmts) carry a cheap hash of their subtree. Equal + // Exprs always have equal hashes, so a mismatch here means we can + // skip the full recursive comparison below. + return false; } else { return equal_impl(a, b); } @@ -65,6 +71,9 @@ bool graph_equal(const IRNode &a, const IRNode &b) { return true; } else if (a.node_type != b.node_type) { return false; + } else if (a.node_type <= StrongestExprNodeType && + ((const BaseExprNode &)a).hash != ((const BaseExprNode &)b).hash) { + return false; } else { return graph_equal_impl(a, b); } @@ -91,6 +100,16 @@ bool less_than(const IRNode &a, const IRNode &b) { return false; } else if (a.node_type < b.node_type) { return true; + } else if (a.node_type == b.node_type && a.node_type <= StrongestExprNodeType) { + // This ordering is arbitrary (it's just used for map keys), so we're + // free to use the cheap hash to distinguish unequal Exprs instead of + // doing a full comparison. + const uint64_t ha = ((const BaseExprNode &)a).hash; + const uint64_t hb = ((const BaseExprNode &)b).hash; + if (ha != hb) { + return ha < hb; + } + return less_than_impl(a, b); } else { return less_than_impl(a, b); } @@ -120,6 +139,13 @@ bool graph_less_than(const IRNode &a, const IRNode &b) { return false; } else if (a.node_type < b.node_type) { return true; + } else if (a.node_type == b.node_type && a.node_type <= StrongestExprNodeType) { + const uint64_t ha = ((const BaseExprNode &)a).hash; + const uint64_t hb = ((const BaseExprNode &)b).hash; + if (ha != hb) { + return ha < hb; + } + return graph_less_than_impl(a, b); } else { return graph_less_than_impl(a, b); } From bc1eaee3aec2101f9fba3b966be2139b2031499d Mon Sep 17 00:00:00 2001 From: Andrew Adams Date: Sun, 6 Sep 2026 16:45:09 -0700 Subject: [PATCH 2/5] Pack the Expr hash into IRNode::node_type's spare bits 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 --- src/Expr.cpp | 8 +++--- src/Expr.h | 56 +++++++++++++++++++++++-------------- src/IR.cpp | 72 +++++++++++++++++++++++++----------------------- src/IREquality.h | 41 ++++++++------------------- src/Type.h | 11 ++++++++ 5 files changed, 98 insertions(+), 90 deletions(-) diff --git a/src/Expr.cpp b/src/Expr.cpp index fd59f14a45fa..abc207a47bd4 100644 --- a/src/Expr.cpp +++ b/src/Expr.cpp @@ -37,7 +37,7 @@ const IntImm *IntImm::make(Type t, int64_t value) { IntImm *node = new IntImm; node->type = t; node->value = value; - node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)value); + node->set_hash(combine_hash((uint32_t)((uint64_t)value >> 32), (uint32_t)value)); return node; } @@ -54,7 +54,7 @@ const UIntImm *UIntImm::make(Type t, uint64_t value) { UIntImm *node = new UIntImm; node->type = t; node->value = value; - node->hash = combine_hash((uint64_t)node->node_type, value); + node->set_hash(combine_hash((uint32_t)(value >> 32), (uint32_t)value)); return node; } @@ -81,7 +81,7 @@ const FloatImm *FloatImm::make(Type t, double value) { internal_error << "FloatImm must be 16, 32, or 64-bit\n"; } - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(node->value)); + node->set_hash((uint32_t)std::hash{}(node->value)); return node; } @@ -89,7 +89,7 @@ const StringImm *StringImm::make(const std::string &val) { StringImm *node = new StringImm; node->type = type_of(); node->value = val; - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(val)); + node->set_hash((uint32_t)std::hash{}(val)); return node; } diff --git a/src/Expr.h b/src/Expr.h index 046bcbe34cdf..1c0f7bdd3be3 100644 --- a/src/Expr.h +++ b/src/Expr.h @@ -104,7 +104,7 @@ struct IRNode { */ virtual void accept(IRVisitor *v) const = 0; IRNode(IRNodeType t) - : node_type(t) { + : hash((uint32_t)t) { } virtual ~IRNode() = default; @@ -122,10 +122,31 @@ struct IRNode { * external libraries compiled without it), and we only want it * for IR nodes. One might want to put this value in the vtable, * but that adds another level of indirection, and for Exprs we - * have 32 free bits in between the ref count and the Type - * anyway, so this doesn't increase the memory footprint of an IR node. - */ - IRNodeType node_type; + * have 32 free bits in between the ref count and the Type field + * anyway, so we use them to also store a cheap hash of the node, + * with the node type packed into the low 8 bits and the rest of + * the hash in the upper 24 bits. This doesn't increase the memory + * footprint of an IR node. The hash is filled in by the make() + * method of each Expr node from the hashes/values of its + * arguments (Stmt nodes leave the upper 24 bits zero). It's not a + * high-quality hash (e.g. it ignores the identity of any + * Buffer/Parameter arguments), but it's cheap enough that + * IREquality.h can use it as a fast pre-check before doing a full + * IR comparison, and it can be used as a hash table key elsewhere, + * so long as some hash collisions are tolerated. */ + union { + IRNodeType node_type; + uint32_t hash; + }; + + /** Set hash from a combined hash of this node's arguments (see + * combine_hash below), keeping the node type in the low 8 bits. The + * low bits of a multiply-add hash are of poor quality, so we discard + * them (rather than shifting them up) in favor of the node type. */ + HALIDE_ALWAYS_INLINE + void set_hash(uint32_t args_hash) { + hash = (args_hash & 0xffffff00u) | (uint32_t)node_type; + } }; template<> @@ -161,27 +182,20 @@ struct BaseExprNode : public IRNode { } virtual Expr mutate_expr(IRMutator *v) const = 0; Type type; - - /** A cheap hash of the node, filled in by the make() method of each - * node from the node type and the hashes/values of its arguments. Not a - * high-quality hash (e.g. it ignores the identity of any Buffer/Parameter - * arguments), but it's cheap enough that it can be used as a fast - * pre-check in IREquality.h before doing a full IR comparison, and as a - * hash table key elsewhere, so long as some hash collisions are tolerated. */ - uint64_t hash = 0; }; -/** Combine one or more child hashes (or plain uint64_t fields) into a - * running hash, for use in the make() methods of Expr nodes below when - * setting BaseExprNode::hash. */ +/** Combine one or more child hashes (or plain uint32_t fields) into a + * running hash, for use in the make() methods of Expr nodes below. Pass + * the result to IRNode::set_hash to fold in the node type and get the + * final hash - see the make() methods below for examples. */ // @{ HALIDE_ALWAYS_INLINE -uint64_t combine_hash(uint64_t hash, uint64_t child_hash) { - return hash * 6364136223846793005ULL + child_hash; +uint32_t combine_hash(uint32_t hash, uint32_t child_hash) { + return hash * 2654435761u + child_hash; } template -HALIDE_ALWAYS_INLINE uint64_t combine_hash(uint64_t hash, uint64_t child_hash, Rest... rest) { +HALIDE_ALWAYS_INLINE uint32_t combine_hash(uint32_t hash, uint32_t child_hash, Rest... rest) { return combine_hash(combine_hash(hash, child_hash), rest...); } // @} @@ -366,9 +380,9 @@ struct Expr : public Internal::IRHandle { return get()->type; } - /** Get the cheap hash of this expression node. See BaseExprNode::hash. */ + /** Get the cheap hash of this expression node. See IRNode::hash. */ HALIDE_ALWAYS_INLINE - uint64_t hash() const { + uint32_t hash() const { return get()->hash; } }; diff --git a/src/IR.cpp b/src/IR.cpp index bb092bd6f9b1..611b0af5a2c9 100644 --- a/src/IR.cpp +++ b/src/IR.cpp @@ -45,7 +45,7 @@ Expr Cast::make(Type t, Expr v) { Cast *node = new Cast; node->type = t; - node->hash = combine_hash((uint64_t)node->node_type, v.hash()); + node->set_hash(combine_hash(v.hash(), t.hash())); node->value = std::move(v); return node; } @@ -62,7 +62,7 @@ Expr Reinterpret::make(Type t, Expr v) { Reinterpret *node = new Reinterpret; node->type = t; - node->hash = combine_hash((uint64_t)node->node_type, v.hash()); + node->set_hash(combine_hash(v.hash(), t.hash())); node->value = std::move(v); return node; } @@ -74,7 +74,7 @@ Expr Add::make(Expr a, Expr b) { Add *node = new Add; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -87,7 +87,7 @@ Expr Sub::make(Expr a, Expr b) { Sub *node = new Sub; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -100,7 +100,7 @@ Expr Mul::make(Expr a, Expr b) { Mul *node = new Mul; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -113,7 +113,7 @@ Expr Div::make(Expr a, Expr b) { Div *node = new Div; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -126,7 +126,7 @@ Expr Mod::make(Expr a, Expr b) { Mod *node = new Mod; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -139,7 +139,7 @@ Expr Min::make(Expr a, Expr b) { Min *node = new Min; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -152,7 +152,7 @@ Expr Max::make(Expr a, Expr b) { Max *node = new Max; node->type = a.type(); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -165,7 +165,7 @@ Expr EQ::make(Expr a, Expr b) { EQ *node = new EQ; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -178,7 +178,7 @@ Expr NE::make(Expr a, Expr b) { NE *node = new NE; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -191,7 +191,7 @@ Expr LT::make(Expr a, Expr b) { LT *node = new LT; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -204,7 +204,7 @@ Expr LE::make(Expr a, Expr b) { LE *node = new LE; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -217,7 +217,7 @@ Expr GT::make(Expr a, Expr b) { GT *node = new GT; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -230,7 +230,7 @@ Expr GE::make(Expr a, Expr b) { GE *node = new GE; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -245,7 +245,7 @@ Expr And::make(Expr a, Expr b) { And *node = new And; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -260,7 +260,7 @@ Expr Or::make(Expr a, Expr b) { Or *node = new Or; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash(), b.hash()); + node->set_hash(combine_hash(a.hash(), b.hash())); node->a = std::move(a); node->b = std::move(b); return node; @@ -272,7 +272,7 @@ Expr Not::make(Expr a) { Not *node = new Not; node->type = Bool(a.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, a.hash()); + node->set_hash(combine_hash(a.hash(), 0)); node->a = std::move(a); return node; } @@ -288,8 +288,8 @@ Expr Select::make(Expr condition, Expr true_value, Expr false_value) { Select *node = new Select; node->type = true_value.type(); - node->hash = combine_hash((uint64_t)node->node_type, condition.hash(), - true_value.hash(), false_value.hash()); + node->set_hash(combine_hash(condition.hash(), + true_value.hash(), false_value.hash())); node->condition = std::move(condition); node->true_value = std::move(true_value); node->false_value = std::move(false_value); @@ -306,8 +306,8 @@ Expr Load::make(Type type, const std::string &name, Expr index, Buffer<> image, Load *node = new Load; node->type = type; node->name = name; - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), - index.hash(), predicate.hash()); + node->set_hash(combine_hash((uint32_t)std::hash{}(name), + index.hash(), predicate.hash())); node->predicate = std::move(predicate); node->index = std::move(index); node->image = std::move(image); @@ -343,8 +343,8 @@ Expr Ramp::make(Expr base, Expr stride, int lanes) { Ramp *node = new Ramp; node->type = base.type().with_lanes(lanes * base.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)lanes, - base.hash(), stride.hash()); + node->set_hash(combine_hash((uint32_t)lanes, + base.hash(), stride.hash())); node->base = std::move(base); node->stride = std::move(stride); node->lanes = lanes; @@ -357,7 +357,7 @@ Expr Broadcast::make(Expr value, int lanes) { Broadcast *node = new Broadcast; node->type = value.type().with_lanes(lanes * value.type().lanes()); - node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)lanes, value.hash()); + node->set_hash(combine_hash((uint32_t)lanes, value.hash())); node->value = std::move(value); node->lanes = lanes; return node; @@ -370,8 +370,8 @@ Expr Let::make(const std::string &name, Expr value, Expr body) { Let *node = new Let; node->type = body.type(); node->name = name; - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), - value.hash(), body.hash()); + node->set_hash(combine_hash((uint32_t)std::hash{}(name), + value.hash(), body.hash())); node->value = std::move(value); node->body = std::move(body); return node; @@ -1002,11 +1002,12 @@ Expr Call::make(Type type, const std::string &name, const std::vector &arg Call *node = new Call; node->type = type; node->name = name; - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name), - (uint64_t)call_type, (uint64_t)value_index); + uint32_t h = combine_hash((uint32_t)std::hash{}(name), + (uint32_t)call_type, (uint32_t)value_index); for (const auto &arg : args) { - node->hash = combine_hash(node->hash, arg.hash()); + h = combine_hash(h, arg.hash()); } + node->set_hash(h); node->args = args; node->call_type = call_type; node->func = std::move(func); @@ -1028,7 +1029,7 @@ Expr Variable::make(Type type, const std::string &name, Buffer<> image, Paramete Variable *node = new Variable; node->type = type; node->name = name; - node->hash = combine_hash((uint64_t)node->node_type, std::hash{}(name)); + node->set_hash((uint32_t)std::hash{}(name)); node->image = std::move(image); node->param = std::move(param); node->reduction_domain = std::move(reduction_domain); @@ -1051,13 +1052,14 @@ Expr Shuffle::make(const std::vector &vectors, Shuffle *node = new Shuffle; node->type = element_ty.with_lanes((int)indices.size()); - node->hash = (uint64_t)node->node_type; + uint32_t h = 0; for (int i : indices) { - node->hash = combine_hash(node->hash, (uint64_t)i); + h = combine_hash(h, (uint32_t)i); } for (const auto &v : vectors) { - node->hash = combine_hash(node->hash, v.hash()); + h = combine_hash(h, v.hash()); } + node->set_hash(h); node->vectors = vectors; node->indices = indices; return node; @@ -1300,7 +1302,7 @@ Expr VectorReduce::make(VectorReduce::Operator op, << lanes << " " << vec.type().lanes() << "\n"; VectorReduce *node = new VectorReduce; node->type = vec.type().with_lanes(lanes); - node->hash = combine_hash((uint64_t)node->node_type, (uint64_t)op, vec.hash()); + node->set_hash(combine_hash((uint32_t)op, vec.hash())); node->op = op; node->value = std::move(vec); return node; diff --git a/src/IREquality.h b/src/IREquality.h index d273f0150f9a..7b33d000b485 100644 --- a/src/IREquality.h +++ b/src/IREquality.h @@ -38,13 +38,11 @@ HALIDE_ALWAYS_INLINE bool equal(const IRNode &a, const IRNode &b) { if (&a == &b) { return true; - } else if (a.node_type != b.node_type) { - return false; - } else if (a.node_type <= StrongestExprNodeType && - ((const BaseExprNode &)a).hash != ((const BaseExprNode &)b).hash) { - // Exprs (unlike Stmts) carry a cheap hash of their subtree. Equal - // Exprs always have equal hashes, so a mismatch here means we can - // skip the full recursive comparison below. + } else if (a.hash != b.hash) { + // IRNode::hash packs the node type into its low 8 bits, so a + // mismatch here also covers the a.node_type != b.node_type case. + // Equal nodes always have equal hashes, so this lets us skip the + // full recursive comparison below. return false; } else { return equal_impl(a, b); @@ -69,10 +67,7 @@ HALIDE_ALWAYS_INLINE bool graph_equal(const IRNode &a, const IRNode &b) { if (&a == &b) { return true; - } else if (a.node_type != b.node_type) { - return false; - } else if (a.node_type <= StrongestExprNodeType && - ((const BaseExprNode &)a).hash != ((const BaseExprNode &)b).hash) { + } else if (a.hash != b.hash) { return false; } else { return graph_equal_impl(a, b); @@ -98,18 +93,11 @@ HALIDE_ALWAYS_INLINE bool less_than(const IRNode &a, const IRNode &b) { if (&a == &b) { return false; - } else if (a.node_type < b.node_type) { - return true; - } else if (a.node_type == b.node_type && a.node_type <= StrongestExprNodeType) { + } else if (a.hash != b.hash) { // This ordering is arbitrary (it's just used for map keys), so we're - // free to use the cheap hash to distinguish unequal Exprs instead of + // free to use the cheap hash to distinguish unequal nodes instead of // doing a full comparison. - const uint64_t ha = ((const BaseExprNode &)a).hash; - const uint64_t hb = ((const BaseExprNode &)b).hash; - if (ha != hb) { - return ha < hb; - } - return less_than_impl(a, b); + return a.hash < b.hash; } else { return less_than_impl(a, b); } @@ -137,15 +125,8 @@ HALIDE_ALWAYS_INLINE bool graph_less_than(const IRNode &a, const IRNode &b) { if (&a == &b) { return false; - } else if (a.node_type < b.node_type) { - return true; - } else if (a.node_type == b.node_type && a.node_type <= StrongestExprNodeType) { - const uint64_t ha = ((const BaseExprNode &)a).hash; - const uint64_t hb = ((const BaseExprNode &)b).hash; - if (ha != hb) { - return ha < hb; - } - return graph_less_than_impl(a, b); + } else if (a.hash != b.hash) { + return a.hash < b.hash; } else { return graph_less_than_impl(a, b); } diff --git a/src/Type.h b/src/Type.h index fae4db772562..a56dd62c810d 100644 --- a/src/Type.h +++ b/src/Type.h @@ -6,6 +6,7 @@ #include "Util.h" #include "runtime/HalideRuntime.h" #include +#include #include /** \file @@ -383,6 +384,16 @@ struct Type { return type_lanes; } + /** A cheap hash of the type, for use in the hashes of Expr nodes that + * embed a Type (see Expr.h). Just the bits of type_code, type_bits, and + * type_lanes (which happen to pack into 32 bits), ignoring handle_index_. */ + HALIDE_ALWAYS_INLINE + uint32_t hash() const { + uint32_t result; + memcpy(&result, this, sizeof(result)); + return result; + } + /** Return Type with same number of bits and lanes, but new_code for a type code. */ HALIDE_ALWAYS_INLINE Type with_code(halide_type_code_t new_code) const { From 3b7820c80796c3a3bd05b1cd31b70fa70c6a0bc3 Mon Sep 17 00:00:00 2001 From: Andrew Adams Date: Sun, 6 Sep 2026 16:47:42 -0700 Subject: [PATCH 3/5] Fix set_hash on big-endian: shift the high-quality bits into place 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 --- src/Expr.h | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Expr.h b/src/Expr.h index 1c0f7bdd3be3..5ded0632b319 100644 --- a/src/Expr.h +++ b/src/Expr.h @@ -124,28 +124,34 @@ struct IRNode { * but that adds another level of indirection, and for Exprs we * have 32 free bits in between the ref count and the Type field * anyway, so we use them to also store a cheap hash of the node, - * with the node type packed into the low 8 bits and the rest of - * the hash in the upper 24 bits. This doesn't increase the memory - * footprint of an IR node. The hash is filled in by the make() - * method of each Expr node from the hashes/values of its - * arguments (Stmt nodes leave the upper 24 bits zero). It's not a - * high-quality hash (e.g. it ignores the identity of any - * Buffer/Parameter arguments), but it's cheap enough that - * IREquality.h can use it as a fast pre-check before doing a full - * IR comparison, and it can be used as a hash table key elsewhere, - * so long as some hash collisions are tolerated. */ + * packed into the same 32-bit word as the node type (see + * set_hash below). This doesn't increase the memory footprint of + * an IR node. The hash is filled in by the make() method of each + * Expr node from the hashes/values of its arguments (Stmt nodes + * leave the rest of the word zero). It's not a high-quality hash + * (e.g. it ignores the identity of any Buffer/Parameter + * arguments), but it's cheap enough that IREquality.h can use it + * as a fast pre-check before doing a full IR comparison, and it + * can be used as a hash table key elsewhere, so long as some hash + * collisions are tolerated. */ union { IRNodeType node_type; uint32_t hash; }; /** Set hash from a combined hash of this node's arguments (see - * combine_hash below), keeping the node type in the low 8 bits. The - * low bits of a multiply-add hash are of poor quality, so we discard - * them (rather than shifting them up) in favor of the node type. */ + * combine_hash below), keeping the node type intact. The low bits of a + * multiply-add hash are of poor quality, so we discard them (rather + * than shifting them up) in favor of the node type. Which end of the + * word the node type landed in when we wrote it via the node_type + * member of the union depends on the endianness of the machine. */ HALIDE_ALWAYS_INLINE void set_hash(uint32_t args_hash) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + hash = (args_hash >> 8) | ((uint32_t)node_type << 24); +#else hash = (args_hash & 0xffffff00u) | (uint32_t)node_type; +#endif } }; From 566b056363eb463f9967d4e62de5de85140ec279 Mon Sep 17 00:00:00 2001 From: Andrew Adams Date: Sun, 6 Sep 2026 17:06:35 -0700 Subject: [PATCH 4/5] Fix (U)IntImm hash discarding small values entirely 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 --- src/Expr.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Expr.cpp b/src/Expr.cpp index abc207a47bd4..84093736d65e 100644 --- a/src/Expr.cpp +++ b/src/Expr.cpp @@ -37,7 +37,12 @@ const IntImm *IntImm::make(Type t, int64_t value) { IntImm *node = new IntImm; node->type = t; node->value = value; - node->set_hash(combine_hash((uint32_t)((uint64_t)value >> 32), (uint32_t)value)); + // Small values are extremely common, so a hash that just slices up the + // bits of the value (like combine_hash below) would put all the entropy + // for those in the low bits, which get discarded by set_hash. Multiply + // by a large odd constant and keep the high bits instead, which mixes + // in the low bits of the value even when the value itself is small. + node->set_hash((uint32_t)((((uint64_t)value) * 0x9e3779b97f4a7c15ULL) >> 32)); return node; } @@ -54,7 +59,9 @@ const UIntImm *UIntImm::make(Type t, uint64_t value) { UIntImm *node = new UIntImm; node->type = t; node->value = value; - node->set_hash(combine_hash((uint32_t)(value >> 32), (uint32_t)value)); + // See the comment in IntImm::make about why we multiply rather than + // just slicing up the bits of the value. + node->set_hash((uint32_t)((value * 0x9e3779b97f4a7c15ULL) >> 32)); return node; } From 2b4367307462757d1f1f643fa36b486acc216256 Mon Sep 17 00:00:00 2001 From: Andrew Adams Date: Sun, 6 Sep 2026 17:13:09 -0700 Subject: [PATCH 5/5] Tighten IRNode hash comment Co-Authored-By: Claude Sonnet 5 --- src/Expr.h | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Expr.h b/src/Expr.h index 5ded0632b319..54aca89b2ebd 100644 --- a/src/Expr.h +++ b/src/Expr.h @@ -115,25 +115,15 @@ struct IRNode { */ mutable RefCount ref_count; - /** Each IR node subclass has a unique identifier. We can compare - * these values to do runtime type identification. We don't - * compile with rtti because that injects run-time type - * identification stuff everywhere (and often breaks when linking - * external libraries compiled without it), and we only want it - * for IR nodes. One might want to put this value in the vtable, - * but that adds another level of indirection, and for Exprs we - * have 32 free bits in between the ref count and the Type field - * anyway, so we use them to also store a cheap hash of the node, - * packed into the same 32-bit word as the node type (see - * set_hash below). This doesn't increase the memory footprint of - * an IR node. The hash is filled in by the make() method of each - * Expr node from the hashes/values of its arguments (Stmt nodes - * leave the rest of the word zero). It's not a high-quality hash - * (e.g. it ignores the identity of any Buffer/Parameter - * arguments), but it's cheap enough that IREquality.h can use it - * as a fast pre-check before doing a full IR comparison, and it - * can be used as a hash table key elsewhere, so long as some hash - * collisions are tolerated. */ + /** Each IR node subclass has a unique identifier. We can compare these + * values to do runtime type identification. We don't compile with rtti + * because that injects run-time type identification stuff everywhere (and + * often breaks when linking external libraries compiled without it), and we + * only want it for IR nodes. One might want to put this value in the + * vtable, but that adds another level of indirection, and for Exprs we have + * 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 { IRNodeType node_type; uint32_t hash;