diff --git a/src/hotspot/share/opto/addnode.cpp b/src/hotspot/share/opto/addnode.cpp index 92fb54a3a130d..c1aa7f3547095 100644 --- a/src/hotspot/share/opto/addnode.cpp +++ b/src/hotspot/share/opto/addnode.cpp @@ -31,8 +31,8 @@ #include "opto/movenode.hpp" #include "opto/mulnode.hpp" #include "opto/phaseX.hpp" +#include "opto/rangeinference.hpp" #include "opto/subnode.hpp" -#include "opto/utilities/xor.hpp" #include "runtime/stubRoutines.hpp" // Portions of code courtesy of Clifford Click @@ -838,35 +838,8 @@ Node* OrINode::Ideal(PhaseGVN* phase, bool can_reshape) { // the logical operations the ring's ADD is really a logical OR function. // This also type-checks the inputs for sanity. Guaranteed never to // be passed a TOP or BOTTOM type, these are filtered out by pre-check. -const Type *OrINode::add_ring( const Type *t0, const Type *t1 ) const { - const TypeInt *r0 = t0->is_int(); // Handy access - const TypeInt *r1 = t1->is_int(); - - // If both args are bool, can figure out better types - if ( r0 == TypeInt::BOOL ) { - if ( r1 == TypeInt::ONE) { - return TypeInt::ONE; - } else if ( r1 == TypeInt::BOOL ) { - return TypeInt::BOOL; - } - } else if ( r0 == TypeInt::ONE ) { - if ( r1 == TypeInt::BOOL ) { - return TypeInt::ONE; - } - } - - // If either input is all ones, the output is all ones. - // x | ~0 == ~0 <==> x | -1 == -1 - if (r0 == TypeInt::MINUS_1 || r1 == TypeInt::MINUS_1) { - return TypeInt::MINUS_1; - } - - // If either input is not a constant, just return all integers. - if( !r0->is_con() || !r1->is_con() ) - return TypeInt::INT; // Any integer, but still no symbols. - - // Otherwise just OR them bits. - return TypeInt::make( r0->get_con() | r1->get_con() ); +const Type* OrINode::add_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_or(t1->is_int(), t2->is_int()); } //============================================================================= @@ -914,22 +887,8 @@ Node* OrLNode::Ideal(PhaseGVN* phase, bool can_reshape) { } //------------------------------add_ring--------------------------------------- -const Type *OrLNode::add_ring( const Type *t0, const Type *t1 ) const { - const TypeLong *r0 = t0->is_long(); // Handy access - const TypeLong *r1 = t1->is_long(); - - // If either input is all ones, the output is all ones. - // x | ~0 == ~0 <==> x | -1 == -1 - if (r0 == TypeLong::MINUS_1 || r1 == TypeLong::MINUS_1) { - return TypeLong::MINUS_1; - } - - // If either input is not a constant, just return all integers. - if( !r0->is_con() || !r1->is_con() ) - return TypeLong::LONG; // Any integer, but still no symbols. - - // Otherwise just OR them bits. - return TypeLong::make( r0->get_con() | r1->get_con() ); +const Type* OrLNode::add_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_or(t1->is_long(), t2->is_long()); } //---------------------------Helper ------------------------------------------- @@ -1016,46 +975,14 @@ const Type* XorINode::Value(PhaseGVN* phase) const { // the logical operations the ring's ADD is really a logical OR function. // This also type-checks the inputs for sanity. Guaranteed never to // be passed a TOP or BOTTOM type, these are filtered out by pre-check. -const Type *XorINode::add_ring( const Type *t0, const Type *t1 ) const { - const TypeInt *r0 = t0->is_int(); // Handy access - const TypeInt *r1 = t1->is_int(); - - if (r0->is_con() && r1->is_con()) { - // compute constant result - return TypeInt::make(r0->get_con() ^ r1->get_con()); - } - - // At least one of the arguments is not constant - - if (r0->_lo >= 0 && r1->_lo >= 0) { - // Combine [r0->_lo, r0->_hi] ^ [r0->_lo, r1->_hi] -> [0, upper_bound] - jint upper_bound = xor_upper_bound_for_ranges(r0->_hi, r1->_hi); - return TypeInt::make(0, upper_bound, MAX2(r0->_widen, r1->_widen)); - } - - return TypeInt::INT; +const Type* XorINode::add_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_xor(t1->is_int(), t2->is_int()); } //============================================================================= //------------------------------add_ring--------------------------------------- -const Type *XorLNode::add_ring( const Type *t0, const Type *t1 ) const { - const TypeLong *r0 = t0->is_long(); // Handy access - const TypeLong *r1 = t1->is_long(); - - if (r0->is_con() && r1->is_con()) { - // compute constant result - return TypeLong::make(r0->get_con() ^ r1->get_con()); - } - - // At least one of the arguments is not constant - - if (r0->_lo >= 0 && r1->_lo >= 0) { - // Combine [r0->_lo, r0->_hi] ^ [r0->_lo, r1->_hi] -> [0, upper_bound] - julong upper_bound = xor_upper_bound_for_ranges(r0->_hi, r1->_hi); - return TypeLong::make(0, upper_bound, MAX2(r0->_widen, r1->_widen)); - } - - return TypeLong::LONG; +const Type* XorLNode::add_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_xor(t1->is_long(), t2->is_long()); } Node* XorLNode::Ideal(PhaseGVN* phase, bool can_reshape) { diff --git a/src/hotspot/share/opto/mulnode.cpp b/src/hotspot/share/opto/mulnode.cpp index 4691c6a45b063..8429ac5a1c6aa 100644 --- a/src/hotspot/share/opto/mulnode.cpp +++ b/src/hotspot/share/opto/mulnode.cpp @@ -29,6 +29,7 @@ #include "opto/memnode.hpp" #include "opto/mulnode.hpp" #include "opto/phaseX.hpp" +#include "opto/rangeinference.hpp" #include "opto/subnode.hpp" #include "utilities/powerOfTwo.hpp" @@ -628,80 +629,14 @@ const Type* MulHiValue(const Type *t1, const Type *t2, const Type *bot) { return TypeLong::LONG; } -template -static const IntegerType* and_value(const IntegerType* r0, const IntegerType* r1) { - typedef typename IntegerType::NativeType NativeType; - static_assert(std::is_signed::value, "Native type of IntegerType must be signed!"); - - int widen = MAX2(r0->_widen, r1->_widen); - - // If both types are constants, we can calculate a constant result. - if (r0->is_con() && r1->is_con()) { - return IntegerType::make(r0->get_con() & r1->get_con()); - } - - // If both ranges are positive, the result will range from 0 up to the hi value of the smaller range. The minimum - // of the two constrains the upper bound because any higher value in the other range will see all zeroes, so it will be masked out. - if (r0->_lo >= 0 && r1->_lo >= 0) { - return IntegerType::make(0, MIN2(r0->_hi, r1->_hi), widen); - } - - // If only one range is positive, the result will range from 0 up to that range's maximum value. - // For the operation 'x & C' where C is a positive constant, the result will be in the range [0..C]. With that observation, - // we can say that for any integer c such that 0 <= c <= C will also be in the range [0..C]. Therefore, 'x & [c..C]' - // where c >= 0 will be in the range [0..C]. - if (r0->_lo >= 0) { - return IntegerType::make(0, r0->_hi, widen); - } - - if (r1->_lo >= 0) { - return IntegerType::make(0, r1->_hi, widen); - } - - // At this point, all positive ranges will have already been handled, so the only remaining cases will be negative ranges - // and constants. - - assert(r0->_lo < 0 && r1->_lo < 0, "positive ranges should already be handled!"); - - // As two's complement means that both numbers will start with leading 1s, the lower bound of both ranges will contain - // the common leading 1s of both minimum values. In order to count them with count_leading_zeros, the bits are inverted. - NativeType sel_val = ~MIN2(r0->_lo, r1->_lo); - - NativeType min; - if (sel_val == 0) { - // Since count_leading_zeros is undefined at 0, we short-circuit the condition where both ranges have a minimum of -1. - min = -1; - } else { - // To get the number of bits to shift, we count the leading 0-bits and then subtract one, as the sign bit is already set. - int shift_bits = count_leading_zeros(sel_val) - 1; - min = std::numeric_limits::min() >> shift_bits; - } - - NativeType max; - if (r0->_hi < 0 && r1->_hi < 0) { - // If both ranges are negative, then the same optimization as both positive ranges will apply, and the smaller hi - // value will mask off any bits set by higher values. - max = MIN2(r0->_hi, r1->_hi); - } else { - // In the case of ranges that cross zero, negative values can cause the higher order bits to be set, so the maximum - // positive value can be as high as the larger hi value. - max = MAX2(r0->_hi, r1->_hi); - } - - return IntegerType::make(min, max, widen); -} - //============================================================================= //------------------------------mul_ring--------------------------------------- // Supplied function returns the product of the inputs IN THE CURRENT RING. // For the logical operations the ring's MUL is really a logical AND function. // This also type-checks the inputs for sanity. Guaranteed never to // be passed a TOP or BOTTOM type, these are filtered out by pre-check. -const Type *AndINode::mul_ring( const Type *t0, const Type *t1 ) const { - const TypeInt* r0 = t0->is_int(); - const TypeInt* r1 = t1->is_int(); - - return and_value(r0, r1); +const Type* AndINode::mul_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_and(t1->is_int(), t2->is_int()); } static bool AndIL_is_zero_element_under_mask(const PhaseGVN* phase, const Node* expr, const Node* mask, BasicType bt); @@ -830,11 +765,8 @@ Node *AndINode::Ideal(PhaseGVN *phase, bool can_reshape) { // For the logical operations the ring's MUL is really a logical AND function. // This also type-checks the inputs for sanity. Guaranteed never to // be passed a TOP or BOTTOM type, these are filtered out by pre-check. -const Type *AndLNode::mul_ring( const Type *t0, const Type *t1 ) const { - const TypeLong* r0 = t0->is_long(); - const TypeLong* r1 = t1->is_long(); - - return and_value(r0, r1); +const Type* AndLNode::mul_ring(const Type* t1, const Type* t2) const { + return RangeInference::infer_and(t1->is_long(), t2->is_long()); } const Type* AndLNode::Value(PhaseGVN* phase) const { diff --git a/src/hotspot/share/opto/rangeinference.cpp b/src/hotspot/share/opto/rangeinference.cpp index 40b9da4bde510..96e77fdfce8df 100644 --- a/src/hotspot/share/opto/rangeinference.cpp +++ b/src/hotspot/share/opto/rangeinference.cpp @@ -25,7 +25,6 @@ #include "opto/rangeinference.hpp" #include "opto/type.hpp" #include "utilities/intn_t.hpp" -#include "utilities/tuple.hpp" // If the cardinality of a TypeInt is below this threshold, use min widen, see // TypeIntPrototype::normalize_widen diff --git a/src/hotspot/share/opto/rangeinference.hpp b/src/hotspot/share/opto/rangeinference.hpp index 9bf252459990f..867c63875d0b1 100644 --- a/src/hotspot/share/opto/rangeinference.hpp +++ b/src/hotspot/share/opto/rangeinference.hpp @@ -26,7 +26,9 @@ #define SHARE_OPTO_RANGEINFERENCE_HPP #include "utilities/globalDefinitions.hpp" +#include "utilities/ostream.hpp" +#include #include class outputStream; @@ -93,19 +95,6 @@ class TypeIntPrototype { RangeInt _urange; KnownBits _bits; -private: - friend class TypeInt; - friend class TypeLong; - - template - friend void test_canonicalize_constraints_exhaustive(); - - template - friend void test_canonicalize_constraints_simple(); - - template - friend void test_canonicalize_constraints_random(); - // A canonicalized version of a TypeIntPrototype, if the prototype represents // an empty type, _present is false, otherwise, _data is canonical class CanonicalizedTypeIntPrototype { @@ -159,21 +148,22 @@ class TypeIntHelper { template static const Type* int_type_xmeet(const CT* i1, const Type* t2); - template - static bool int_type_is_equal(const CT* t1, const CT* t2) { + template + static bool int_type_is_equal(const CTP t1, const CTP t2) { return t1->_lo == t2->_lo && t1->_hi == t2->_hi && t1->_ulo == t2->_ulo && t1->_uhi == t2->_uhi && t1->_bits._zeros == t2->_bits._zeros && t1->_bits._ones == t2->_bits._ones; } - template - static bool int_type_is_subset(const CT* super, const CT* sub) { + template + static bool int_type_is_subset(const CTP super, const CTP sub) { + using U = decltype(super->_ulo); return super->_lo <= sub->_lo && super->_hi >= sub->_hi && super->_ulo <= sub->_ulo && super->_uhi >= sub->_uhi && // All bits that are known in super must also be known to be the same // value in sub, &~ (and not) is the same as a set subtraction on bit // sets - (super->_bits._zeros &~ sub->_bits._zeros) == 0 && (super->_bits._ones &~ sub->_bits._ones) == 0; + (super->_bits._zeros &~ sub->_bits._zeros) == U(0) && (super->_bits._ones &~ sub->_bits._ones) == U(0); } template @@ -196,4 +186,202 @@ class TypeIntHelper { #endif // PRODUCT }; +// A TypeIntMirror is structurally similar to a TypeInt or a TypeLong but it decouples the range +// inference from the Type infrastructure of the compiler. It also allows more flexibility with the +// bit width of the integer type. As a result, it is more efficient to use for intermediate steps +// of inference, as well as more flexible to perform testing on different integer types. +template +class TypeIntMirror { +public: + S _lo; + S _hi; + U _ulo; + U _uhi; + KnownBits _bits; + int _widen = 0; // dummy field to mimic the same field in TypeInt, useful in testing + + static TypeIntMirror make(const TypeIntPrototype& t, int widen) { + auto canonicalized_t = t.canonicalize_constraints(); + assert(!canonicalized_t.empty(), "must not be empty"); + return TypeIntMirror{canonicalized_t._data._srange._lo, canonicalized_t._data._srange._hi, + canonicalized_t._data._urange._lo, canonicalized_t._data._urange._hi, + canonicalized_t._data._bits}; + } + + // These allow TypeIntMirror to mimick the behaviors of TypeInt* and TypeLong*, so they can be + // passed into RangeInference methods. These are only used in testing, so they are implemented in + // the test file. + const TypeIntMirror* operator->() const; + TypeIntMirror meet(const TypeIntMirror& o) const; + bool contains(U u) const; + bool contains(const TypeIntMirror& o) const; + bool operator==(const TypeIntMirror& o) const; + + template + TypeIntMirror cast() const; +}; + +// This class contains methods for inferring the Type of the result of several arithmetic +// operations from those of the corresponding inputs. For example, given a, b such that the Type of +// a is [0, 1] and the Type of b is [-1, 3], then the Type of the sum a + b is [-1, 4]. +// The methods in this class receive one or more template parameters which are often TypeInt* or +// TypeLong*, or they can be TypeIntMirror which behave similar to TypeInt* and TypeLong* during +// testing. This allows us to verify the correctness of the implementation without coupling with +// the hotspot compiler allocation infrastructure. +class RangeInference { +private: + // If CTP is a pointer, get the underlying type. For the test helper classes, using the struct + // directly allows straightfoward equality comparison. + template + using CT = std::remove_const_t, std::remove_pointer_t, CTP>>; + + // The type of CT::_lo, should be jint for TypeInt* and jlong for TypeLong* + template + using S = std::remove_const_t::_lo)>; + + // The type of CT::_ulo, should be juint for TypeInt* and julong for TypeLong* + template + using U = std::remove_const_t::_ulo)>; + + // A TypeInt consists of 1 or 2 simple intervals, each of which will lie either in the interval + // [0, max_signed] or [min_signed, -1]. It is more optimal to analyze each simple interval + // separately when doing inference. For example, consider a, b whose Types are both [-2, 2]. By + // analyzing the interval [-2, -1] and [0, 2] separately, we can easily see that the result of + // a & b must also be in the interval [-2, 2]. This is much harder if we want to work with the + // whole value range at the same time. + // This class offers a convenient way to traverse all the simple interval of a TypeInt. + template + class SimpleIntervalIterable { + private: + TypeIntMirror, U> _first_interval; + TypeIntMirror, U> _second_interval; + int _interval_num; + + public: + SimpleIntervalIterable(CTP t) { + if (U(t->_lo) <= U(t->_hi)) { + _interval_num = 1; + _first_interval = TypeIntMirror, U>{t->_lo, t->_hi, t->_ulo, t->_uhi, t->_bits}; + } else { + _interval_num = 2; + _first_interval = TypeIntMirror, U>::make(TypeIntPrototype, U>{{t->_lo, S(t->_uhi)}, {U(t->_lo), t->_uhi}, t->_bits}, 0); + _second_interval = TypeIntMirror, U>::make(TypeIntPrototype, U>{{S(t->_ulo), t->_hi}, {t->_ulo, U(t->_hi)}, t->_bits}, 0); + } + } + + class Iterator { + private: + const SimpleIntervalIterable& _iterable; + int _current_interval; + + Iterator(const SimpleIntervalIterable& iterable) : _iterable(iterable), _current_interval(0) {} + + friend class SimpleIntervalIterable; + public: + const TypeIntMirror, U>& operator*() const { + assert(_current_interval < _iterable._interval_num, "out of bounds, %d - %d", _current_interval, _iterable._interval_num); + if (_current_interval == 0) { + return _iterable._first_interval; + } else { + return _iterable._second_interval; + } + } + + Iterator& operator++() { + assert(_current_interval < _iterable._interval_num, "out of bounds, %d - %d", _current_interval, _iterable._interval_num); + _current_interval++; + return *this; + } + + Iterator operator++(int) { + assert(_current_interval < _iterable._interval_num, "out of bounds, %d - %d", _current_interval, _iterable._interval_num); + Iterator res(*this); + _current_interval++; + return res; + } + + bool operator!=(const Iterator& o) const { + assert(&_iterable == &o._iterable, "not on the same iterable"); + return _current_interval != o._current_interval; + } + }; + + Iterator begin() const { + return Iterator(*this); + } + + Iterator end() const { + Iterator res(*this); + res._current_interval = _interval_num; + return res; + } + }; + + // Infer a result given the input types of a binary operation + template + static CTP infer_binary(CTP t1, CTP t2, Inference infer) { + CTP res; + bool init = false; + + SimpleIntervalIterable t1_simple_intervals(t1); + SimpleIntervalIterable t2_simple_intervals(t2); + + for (auto& st1 : t1_simple_intervals) { + for (auto& st2 : t2_simple_intervals) { + CTP current = infer(st1, st2); + + if (init) { + res = res->meet(current)->template cast>(); + } else { + init = true; + res = current; + } + } + } + + assert(init, "must be initialized"); + return res; + } + +public: + template + static CTP infer_and(CTP t1, CTP t2) { + return infer_binary(t1, t2, [&](const TypeIntMirror, U>& st1, const TypeIntMirror, U>& st2) { + S lo = std::numeric_limits>::min(); + S hi = std::numeric_limits>::max(); + U ulo = std::numeric_limits>::min(); + U uhi = MIN2(st1._uhi, st2._uhi); + U zeros = st1._bits._zeros | st2._bits._zeros; + U ones = st1._bits._ones & st2._bits._ones; + return CT::make(TypeIntPrototype, U>{{lo, hi}, {ulo, uhi}, {zeros, ones}}, MAX2(t1->_widen, t2->_widen)); + }); + } + + template + static CTP infer_or(CTP t1, CTP t2) { + return infer_binary(t1, t2, [&](const TypeIntMirror, U>& st1, const TypeIntMirror, U>& st2) { + S lo = std::numeric_limits>::min(); + S hi = std::numeric_limits>::max(); + U ulo = MAX2(st1._ulo, st2._ulo); + U uhi = std::numeric_limits>::max(); + U zeros = st1._bits._zeros & st2._bits._zeros; + U ones = st1._bits._ones | st2._bits._ones; + return CT::make(TypeIntPrototype, U>{{lo, hi}, {ulo, uhi}, {zeros, ones}}, MAX2(t1->_widen, t2->_widen)); + }); + } + + template + static CTP infer_xor(CTP t1, CTP t2) { + return infer_binary(t1, t2, [&](const TypeIntMirror, U>& st1, const TypeIntMirror, U>& st2) { + S lo = std::numeric_limits>::min(); + S hi = std::numeric_limits>::max(); + U ulo = std::numeric_limits>::min(); + U uhi = std::numeric_limits>::max(); + U zeros = (st1._bits._zeros & st2._bits._zeros) | (st1._bits._ones & st2._bits._ones); + U ones = (st1._bits._zeros & st2._bits._ones) | (st1._bits._ones & st2._bits._zeros); + return CT::make(TypeIntPrototype, U>{{lo, hi}, {ulo, uhi}, {zeros, ones}}, MAX2(t1->_widen, t2->_widen)); + }); + } +}; + #endif // SHARE_OPTO_RANGEINFERENCE_HPP diff --git a/src/hotspot/share/opto/type.hpp b/src/hotspot/share/opto/type.hpp index c61c2a6427830..28a065d7c3da3 100644 --- a/src/hotspot/share/opto/type.hpp +++ b/src/hotspot/share/opto/type.hpp @@ -798,6 +798,7 @@ class TypeInt : public TypeInteger { // must always specify w static const TypeInt* make(jint lo, jint hi, int widen); static const Type* make_or_top(const TypeIntPrototype& t, int widen); + static const TypeInt* make(const TypeIntPrototype& t, int widen) { return make_or_top(t, widen)->is_int(); } // Check for single integer bool is_con() const { return _lo == _hi; } @@ -879,6 +880,7 @@ class TypeLong : public TypeInteger { // must always specify w static const TypeLong* make(jlong lo, jlong hi, int widen); static const Type* make_or_top(const TypeIntPrototype& t, int widen); + static const TypeLong* make(const TypeIntPrototype& t, int widen) { return make_or_top(t, widen)->is_long(); } // Check for single integer bool is_con() const { return _lo == _hi; } diff --git a/src/hotspot/share/opto/utilities/xor.hpp b/src/hotspot/share/opto/utilities/xor.hpp deleted file mode 100644 index 20edaf0d01779..0000000000000 --- a/src/hotspot/share/opto/utilities/xor.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef SHARE_OPTO_UTILITIES_XOR_HPP -#define SHARE_OPTO_UTILITIES_XOR_HPP - -#include "utilities/powerOfTwo.hpp" -// Code separated into its own header to allow access from GTEST - -// Given 2 non-negative values in the ranges [0, hi_0] and [0, hi_1], respectively. The bitwise -// xor of these values should also be non-negative. This method calculates an upper bound. - -// S and U type parameters correspond to the signed and unsigned -// variants of an integer to operate on. -template -static S xor_upper_bound_for_ranges(const S hi_0, const S hi_1) { - static_assert(S(-1) < S(0), "S must be signed"); - static_assert(U(-1) > U(0), "U must be unsigned"); - - assert(hi_0 >= 0, "must be non-negative"); - assert(hi_1 >= 0, "must be non-negative"); - - // x ^ y cannot have any bit set that is higher than both the highest bits set in x and y - // x cannot have any bit set that is higher than the highest bit set in hi_0 - // y cannot have any bit set that is higher than the highest bit set in hi_1 - - // We want to find a value that has all 1 bits everywhere up to and including - // the highest bits set in hi_0 as well as hi_1. For this, we can take the next - // power of 2 strictly greater than both hi values and subtract 1 from it. - - // Example 1: - // hi_0 = 5 (0b0101) hi_1=1 (0b0001) - // (5|1)+1 = 0b0110 - // round_up_pow2 = 0b1000 - // -1 = 0b0111 = max - - // Example 2 - this demonstrates need for the +1: - // hi_0 = 4 (0b0100) hi_1=4 (0b0100) - // (4|4)+1 = 0b0101 - // round_up_pow2 = 0b1000 - // -1 = 0b0111 = max - // Without the +1, round_up_pow2 would be 0b0100, resulting in 0b0011 as max - - // Note: cast to unsigned happens before +1 to avoid signed overflow, and - // round_up is safe because high bit is unset (0 <= lo <= hi) - - return round_up_power_of_2(U(hi_0 | hi_1) + 1) - 1; -} - -#endif // SHARE_OPTO_UTILITIES_XOR_HPP diff --git a/test/hotspot/gtest/opto/test_rangeinference.cpp b/test/hotspot/gtest/opto/test_rangeinference.cpp index 1a62941a48620..142880fbcf4a9 100644 --- a/test/hotspot/gtest/opto/test_rangeinference.cpp +++ b/test/hotspot/gtest/opto/test_rangeinference.cpp @@ -27,13 +27,14 @@ #include "runtime/os.hpp" #include "utilities/intn_t.hpp" #include "unittest.hpp" +#include +#include +#include +#include template -static U uniform_random(); - -template <> -juint uniform_random() { - return os::random(); +static U uniform_random() { + return U(juint(os::random())); } template <> @@ -201,7 +202,7 @@ static void test_canonicalize_constraints_random() { } } -TEST_VM(opto, canonicalize_constraints) { +TEST(opto, canonicalize_constraints) { test_canonicalize_constraints_trivial(); test_canonicalize_constraints_exhaustive, uintn_t<1>>(); test_canonicalize_constraints_exhaustive, uintn_t<2>>(); @@ -212,3 +213,385 @@ TEST_VM(opto, canonicalize_constraints) { test_canonicalize_constraints_random(); test_canonicalize_constraints_random(); } + +template +const TypeIntMirror* TypeIntMirror::operator->() const { + return this; +} + +template +TypeIntMirror TypeIntMirror::meet(const TypeIntMirror& o) const { + return make(TypeIntPrototype{{MIN2(_lo, o._lo), MAX2(_hi, o._hi)}, + {MIN2(_ulo, o._ulo), MAX2(_uhi, o._uhi)}, + {_bits._zeros & o._bits._zeros, _bits._ones & o._bits._ones}}, 0); +} + +template +bool TypeIntMirror::contains(U u) const { + S s = S(u); + return s >= _lo && s <= _hi && u >= _ulo && u <= _uhi && _bits.is_satisfied_by(u); +} + +template +bool TypeIntMirror::contains(const TypeIntMirror& o) const { + return TypeIntHelper::int_type_is_subset(*this, o); +} + +template +bool TypeIntMirror::operator==(const TypeIntMirror& o) const { + return TypeIntHelper::int_type_is_equal(*this, o); +} + +template +template +TypeIntMirror TypeIntMirror::cast() const { + static_assert(std::is_same_v); + return *this; +} + +template +class std::hash> { +public: + size_t operator()(const TypeIntMirror& t) const noexcept { + return (julong(juint(U(t._lo))) << 40) | (julong(juint(U(t._hi))) << 32) | + (julong(juint(t._ulo)) << 24) | (julong(juint(t._ulo)) << 16) | + (julong(juint(t._bits._zeros)) << 8) | julong(juint(t._bits._ones)); + } +}; + + +template +static constexpr size_t all_instances_size() { + using U = decltype(CTP::_ulo); + constexpr juint max_unsigned = juint(std::numeric_limits::max()); + if constexpr (max_unsigned == 1U) { + return 3; + } else if constexpr (max_unsigned == 3U) { + return 15; + } else if constexpr (max_unsigned == 7U) { + return 134; + } else { + static_assert(max_unsigned == 15U); + return 1732; + } +} + +template +static std::array()> compute_all_instances() { + using S = decltype(CTP::_lo); + using U = decltype(CTP::_ulo); + + std::unordered_set collector; + collector.reserve(all_instances_size()); + for (jint lo = jint(std::numeric_limits::min()); lo <= jint(std::numeric_limits::max()); lo++) { + for (jint hi = lo; hi <= jint(std::numeric_limits::max()); hi++) { + for (juint ulo = 0; ulo <= juint(std::numeric_limits::max()); ulo++) { + for (juint uhi = ulo; uhi <= juint(std::numeric_limits::max()); uhi++) { + for (juint zeros = 0; zeros <= juint(std::numeric_limits::max()); zeros++) { + for (juint ones = 0; ones <= juint(std::numeric_limits::max()); ones++) { + TypeIntPrototype t{{S(lo), S(hi)}, {U(ulo), U(uhi)}, {U(zeros), U(ones)}}; + auto canonicalized_t = t.canonicalize_constraints(); + if (canonicalized_t.empty()) { + continue; + } + + TypeIntPrototype ct = canonicalized_t._data; + collector.insert(CTP{ct._srange._lo, ct._srange._hi, ct._urange._lo, ct._urange._hi, ct._bits}); + } + } + } + } + } + } + + assert(collector.size() == all_instances_size(), "unexpected size of all_instance, expected %d, actual %d", jint(all_instances_size()), jint(collector.size())); + std::array()> res; + size_t idx = 0; + for (const CTP& t : collector) { + res[idx] = t; + idx++; + } + return res; +} + +template +static const std::array()>& all_instances() { + static std::array()> res = compute_all_instances(); + return res; +} + +// Check the correctness, that is, if v1 is an element of input1, v2 is an element of input2, then +// op(v1, v2) must be an element of infer(input1, input2). This version does the check exhaustively +// on all elements of input1 and input2. +template +static void test_binary_instance_correctness_exhaustive(Operation op, Inference infer, const InputType& input1, const InputType& input2) { + using S = std::remove_const_t_lo)>; + using U = std::remove_const_t_ulo)>; + InputType result = infer(input1, input2); + + for (juint v1 = juint(std::numeric_limits::min()); v1 <= juint(std::numeric_limits::max()); v1++) { + if (!input1.contains(U(v1))) { + continue; + } + + for (juint v2 = juint(std::numeric_limits::min()); v2 <= juint(std::numeric_limits::max()); v2++) { + if (!input2.contains(U(v2))) { + continue; + } + + U r = op(U(v1), U(v2)); + ASSERT_TRUE(result.contains(r)); + } + } +} + +// Check the correctness, that is, if v1 is an element of input1, v2 is an element of input2, then +// op(v1, v2) must be an element of infer(input1, input2). This version does the check randomly on +// a number of elements in input1 and input2. +template +static void test_binary_instance_correctness_samples(Operation op, Inference infer, const InputType& input1, const InputType& input2) { + using U = std::remove_const_t_ulo)>; + auto result = infer(input1, input2); + + constexpr size_t sample_count = 6; + U input1_samples[sample_count] {U(input1._lo), U(input1._hi), input1._ulo, input1._uhi, input1._ulo, input1._ulo}; + U input2_samples[sample_count] {U(input2._lo), U(input2._hi), input2._ulo, input2._uhi, input2._ulo, input2._ulo}; + + auto random_sample = [](U* samples, const InputType& input) { + constexpr size_t max_tries = 100; + constexpr size_t start_random_idx = 4; + for (size_t tries = 0, idx = start_random_idx; tries < max_tries && idx < sample_count; tries++) { + U n = uniform_random(); + if (input.contains(n)) { + samples[idx] = n; + idx++; + } + } + }; + random_sample(input1_samples, input1); + random_sample(input2_samples, input2); + + for (size_t i = 0; i < sample_count; i++) { + for (size_t j = 0; j < sample_count; j++) { + U r = op(input1_samples[i], input2_samples[j]); + ASSERT_TRUE(result.contains(r)); + } + } +} + +// Check the monotonicity, that is, if input1 is a subset of super1, input2 is a subset of super2, +// then infer(input1, input2) must be a subset of infer(super1, super2). This version does the +// check exhaustively on all supersets of input1 and input2. +template +static void test_binary_instance_monotonicity_exhaustive(Inference infer, const InputType& input1, const InputType& input2) { + InputType result = infer(input1, input2); + + for (const InputType& super1 : all_instances()) { + if (!super1.contains(input1) || super1 == input1) { + continue; + } + + for (const InputType& super2 : all_instances()) { + if (!super2.contains(input2) || super2 == input2) { + continue; + } + + ASSERT_TRUE(infer(input1, super2).contains(result)); + ASSERT_TRUE(infer(super1, input2).contains(result)); + ASSERT_TRUE(infer(super1, super2).contains(result)); + } + } +} + +// Check the monotonicity, that is, if input1 is a subset of super1, input2 is a subset of super2, +// then infer(input1, input2) must be a subset of infer(super1, super2). This version does the +// check randomly on a number of supersets of input1 and input2. +template +static void test_binary_instance_monotonicity_samples(Inference infer, const InputType& input1, const InputType& input2) { + using S = std::remove_const_t_lo)>; + using U = std::remove_const_t_ulo)>; + auto result = infer(input1, input2); + + // The set that is a superset of all other sets + InputType universe = InputType{std::numeric_limits::min(), std::numeric_limits::max(), U(0), U(-1), {U(0), U(0)}}; + ASSERT_TRUE(infer(universe, input2).contains(result)); + ASSERT_TRUE(infer(input1, universe).contains(result)); + ASSERT_TRUE(infer(universe, universe).contains(result)); + + auto random_superset = [](const InputType& input) { + S lo = MIN2(input->_lo, S(uniform_random())); + S hi = MAX2(input->_hi, S(uniform_random())); + U ulo = MIN2(input->_ulo, uniform_random()); + U uhi = MAX2(input->_uhi, uniform_random()); + U zeros = input->_bits._zeros & uniform_random(); + U ones = input->_bits._ones & uniform_random(); + InputType super = InputType::make(TypeIntPrototype{{lo, hi}, {ulo, uhi}, {zeros, ones}}, 0); + assert(super.contains(input), "impossible"); + return super; + }; + + InputType super1 = random_superset(input1); + InputType super2 = random_superset(input2); + ASSERT_TRUE(infer(super1, input2).contains(result)); + ASSERT_TRUE(infer(input1, super2).contains(result)); + ASSERT_TRUE(infer(super1, super2).contains(result)); +} + +// Verify the correctness and monotonicity of an inference function by exhautively analyzing all +// instances of InputType +template +static void test_binary_exhaustive(Operation op, Inference infer) { + for (const InputType& input1 : all_instances()) { + for (const InputType& input2 : all_instances()) { + test_binary_instance_correctness_exhaustive(op, infer, input1, input2); + if (all_instances().size() < 100) { + // This effectively covers the cases up to uintn_t<2> + test_binary_instance_monotonicity_exhaustive(infer, input1, input2); + } else { + // This effectively covers the cases of uintn_t<3> + test_binary_instance_monotonicity_samples(infer, input1, input2); + } + } + } +} + +// Verify the correctness and monotonicity of an inference function by randomly sampling instances +// of InputType +template +static void test_binary_random(Operation op, Inference infer) { + using S = std::remove_const_t; + using U = std::remove_const_t; + + constexpr size_t sample_count = 100; + InputType samples[sample_count]; + + // Fill with {0} + for (size_t i = 0; i < sample_count; i++) { + samples[i] = InputType::make(TypeIntPrototype{{S(0), S(0)}, {U(0), U(0)}, {U(0), U(0)}}, 0); + } + + // {1} + samples[1] = InputType::make(TypeIntPrototype{{S(1), S(1)}, {U(1), U(1)}, {U(0), U(0)}}, 0); + // {-1} + samples[2] = InputType::make(TypeIntPrototype{{S(-1), S(-1)}, {U(-1), U(-1)}, {U(0), U(0)}}, 0); + // {0, 1} + samples[3] = InputType::make(TypeIntPrototype{{S(0), S(1)}, {U(0), U(1)}, {U(0), U(0)}}, 0); + // {-1, 0, 1} + samples[4] = InputType::make(TypeIntPrototype{{S(-1), S(1)}, {U(0), U(-1)}, {U(0), U(0)}}, 0); + // {-1, 1} + samples[5] = InputType::make(TypeIntPrototype{{S(-1), S(1)}, {U(1), U(-1)}, {U(0), U(0)}}, 0); + // {0, 1, 2} + samples[6] = InputType::make(TypeIntPrototype{{S(0), S(2)}, {U(0), U(2)}, {U(0), U(0)}}, 0); + // {0, 2} + samples[7] = InputType::make(TypeIntPrototype{{S(0), S(2)}, {U(0), U(2)}, {U(1), U(0)}}, 0); + // [min_signed, max_signed] + samples[8] = InputType::make(TypeIntPrototype{{std::numeric_limits::min(), std::numeric_limits::max()}, {U(0), U(-1)}, {U(0), U(0)}}, 0); + // [0, max_signed] + samples[9] = InputType::make(TypeIntPrototype{{0, std::numeric_limits::max()}, {U(0), U(-1)}, {U(0), U(0)}}, 0); + // [min_signed, 0) + samples[10] = InputType::make(TypeIntPrototype{{std::numeric_limits::min(), -1}, {U(0), U(-1)}, {U(0), U(0)}}, 0); + + constexpr size_t max_tries = 1000; + constexpr size_t start_random_idx = 11; + for (size_t tries = 0, idx = start_random_idx; tries < max_tries && idx < sample_count; tries++) { + // Try to have lo < hi + S signed_bound1 = S(uniform_random()); + S signed_bound2 = S(uniform_random()); + S lo = MIN2(signed_bound1, signed_bound2); + S hi = MAX2(signed_bound1, signed_bound2); + + // Try to have ulo < uhi + U unsigned_bound1 = uniform_random(); + U unsigned_bound2 = uniform_random(); + U ulo = MIN2(unsigned_bound1, unsigned_bound2); + U uhi = MAX2(unsigned_bound1, unsigned_bound2); + + // Try to have (zeros & ones) == 0 + U zeros = uniform_random(); + U ones = uniform_random(); + U common = zeros & ones; + zeros ^= common; + ones ^= common; + + TypeIntPrototype t{{lo, hi}, {ulo, uhi}, {zeros, ones}}; + auto canonicalized_t = t.canonicalize_constraints(); + if (canonicalized_t.empty()) { + continue; + } + + samples[idx] = TypeIntMirror{canonicalized_t._data._srange._lo, canonicalized_t._data._srange._hi, + canonicalized_t._data._urange._lo, canonicalized_t._data._urange._hi, + canonicalized_t._data._bits}; + idx++; + } + + for (size_t i = 0; i < sample_count; i++) { + for (size_t j = 0; j < sample_count; j++) { + test_binary_instance_correctness_samples(op, infer, samples[i], samples[j]); + test_binary_instance_monotonicity_samples(infer, samples[i], samples[j]); + } + } +} + +template