From ee7b2e9b3bd2b4607476ae03bb6c9f7e49a43d0e Mon Sep 17 00:00:00 2001 From: Coleen Phillimore Date: Mon, 22 Apr 2024 11:25:53 +0000 Subject: [PATCH] 8330051: Small ObjectMonitor spinning code cleanups Reviewed-by: dcubed, eosterlund, fbredberg --- src/hotspot/share/runtime/objectMonitor.cpp | 173 +++++++++++--------- src/hotspot/share/runtime/objectMonitor.hpp | 10 +- 2 files changed, 106 insertions(+), 77 deletions(-) diff --git a/src/hotspot/share/runtime/objectMonitor.cpp b/src/hotspot/share/runtime/objectMonitor.cpp index b1fe22444ae..d281acdf006 100644 --- a/src/hotspot/share/runtime/objectMonitor.cpp +++ b/src/hotspot/share/runtime/objectMonitor.cpp @@ -108,19 +108,6 @@ #endif // ndef DTRACE_ENABLED -// Tunables ... -// The knob* variables are effectively final. Once set they should -// never be modified hence. Consider using __read_mostly with GCC. - -int ObjectMonitor::Knob_SpinLimit = 5000; // derived by an external tool - - -static int Knob_Bonus = 100; // spin success bonus -static int Knob_BonusB = 100; // spin success bonus -static int Knob_Penalty = 200; // spin failure penalty -static int Knob_Poverty = 1000; -static int Knob_FixedSpin = 0; -static int Knob_PreSpin = 10; // 20-100 likely better - DEBUG_ONLY(static volatile bool InitDone = false;) OopStorage* ObjectMonitor::_oop_storage = nullptr; @@ -406,7 +393,7 @@ bool ObjectMonitor::enter(JavaThread* current) { // transitions. The following spin is strictly optional ... // Note that if we acquire the monitor from an initial spin // we forgo posting JVMTI events and firing DTRACE probes. - if (TrySpin(current) > 0) { + if (TrySpin(current)) { assert(owner_raw() == current, "must be current: owner=" INTPTR_FORMAT, p2i(owner_raw())); assert(_recursions == 0, "must be 0: recursions=" INTX_FORMAT, _recursions); assert(object()->mark() == markWord::encode(this), @@ -535,18 +522,18 @@ bool ObjectMonitor::enter(JavaThread* current) { // Caveat: TryLock() is not necessarily serializing if it returns failure. // Callers must compensate as needed. -int ObjectMonitor::TryLock(JavaThread* current) { +ObjectMonitor::TryLockResult ObjectMonitor::TryLock(JavaThread* current) { void* own = owner_raw(); - if (own != nullptr) return 0; + if (own != nullptr) return TryLockResult::HasOwner; if (try_set_owner_from(nullptr, current) == nullptr) { assert(_recursions == 0, "invariant"); - return 1; + return TryLockResult::Success; } // The lock had been free momentarily, but we lost the race to the lock. // Interference -- the CAS failed. // We can either return -1 or retry. // Retry doesn't make as much sense because the lock was just acquired. - return -1; + return TryLockResult::Interference; } // Deflate the specified ObjectMonitor if not in-use. Returns true if it @@ -723,7 +710,7 @@ void ObjectMonitor::EnterI(JavaThread* current) { assert(current->thread_state() == _thread_blocked, "invariant"); // Try the lock - TATAS - if (TryLock (current) > 0) { + if (TryLock(current) == TryLockResult::Success) { assert(_succ != current, "invariant"); assert(owner_raw() == current, "invariant"); assert(_Responsible != current, "invariant"); @@ -757,7 +744,7 @@ void ObjectMonitor::EnterI(JavaThread* current) { // to the owner. This has subtle but beneficial affinity // effects. - if (TrySpin(current) > 0) { + if (TrySpin(current)) { assert(owner_raw() == current, "invariant"); assert(_succ != current, "invariant"); assert(_Responsible != current, "invariant"); @@ -794,7 +781,7 @@ void ObjectMonitor::EnterI(JavaThread* current) { // Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. - if (TryLock (current) > 0) { + if (TryLock(current) == TryLockResult::Success) { assert(_succ != current, "invariant"); assert(owner_raw() == current, "invariant"); assert(_Responsible != current, "invariant"); @@ -847,7 +834,9 @@ void ObjectMonitor::EnterI(JavaThread* current) { for (;;) { - if (TryLock(current) > 0) break; + if (TryLock(current) == TryLockResult::Success) { + break; + } assert(owner_raw() != current, "invariant"); // park self @@ -862,7 +851,9 @@ void ObjectMonitor::EnterI(JavaThread* current) { current->_ParkEvent->park(); } - if (TryLock(current) > 0) break; + if (TryLock(current) == TryLockResult::Success) { + break; + } if (try_set_owner_from(DEFLATER_MARKER, current) == DEFLATER_MARKER) { // Cancelled the in-progress async deflation by changing owner from @@ -895,7 +886,9 @@ void ObjectMonitor::EnterI(JavaThread* current) { // We can defer clearing _succ until after the spin completes // TrySpin() must tolerate being called with _succ == current. // Try yet another round of adaptive spinning. - if (TrySpin(current) > 0) break; + if (TrySpin(current)) { + break; + } // We can find that we were unpark()ed and redesignated _succ while // we were spinning. That's harmless. If we iterate and call park(), @@ -994,8 +987,9 @@ void ObjectMonitor::ReenterI(JavaThread* current, ObjectWaiter* currentNode) { guarantee(v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant"); assert(owner_raw() != current, "invariant"); - if (TryLock(current) > 0) break; - if (TrySpin(current) > 0) break; + if (TrySpin(current)) { + break; + } { OSThreadContendState osts(current->osthread()); @@ -1012,7 +1006,9 @@ void ObjectMonitor::ReenterI(JavaThread* current, ObjectWaiter* currentNode) { // Try again, but just so we distinguish between futile wakeups and // successful wakeups. The following test isn't algorithmically // necessary, but it helps us maintain sensible statistics. - if (TryLock(current) > 0) break; + if (TryLock(current) == TryLockResult::Success) { + break; + } // The lock is still contested. // Keep a tally of the # of futile wakeups. @@ -1854,32 +1850,65 @@ void ObjectMonitor::notifyAll(TRAPS) { // hysteresis control to damp the transition rate between spinning and // not spinning. -// Spinning: Fixed frequency (100%), vary duration -int ObjectMonitor::TrySpin(JavaThread* current) { - // Dumb, brutal spin. Good for comparative measurements against adaptive spinning. - int ctr = Knob_FixedSpin; - if (ctr != 0) { - while (--ctr >= 0) { - if (TryLock(current) > 0) return 1; - SpinPause(); - } - return 0; - } +int ObjectMonitor::Knob_SpinLimit = 5000; // derived by an external tool - for (ctr = Knob_PreSpin + 1; --ctr >= 0;) { - if (TryLock(current) > 0) { - // Increase _SpinDuration ... - // Note that we don't clamp SpinDuration precisely at SpinLimit. - // Raising _SpurDuration to the poverty line is key. - int x = _SpinDuration; - if (x < Knob_SpinLimit) { - if (x < Knob_Poverty) x = Knob_Poverty; - _SpinDuration = x + Knob_BonusB; +static int Knob_Bonus = 100; // spin success bonus +static int Knob_Penalty = 200; // spin failure penalty +static int Knob_Poverty = 1000; +static int Knob_FixedSpin = 0; +static int Knob_PreSpin = 10; // 20-100 likely better, but it's not better in my testing. + +inline static int adjust_up(int spin_duration) { + int x = spin_duration; + if (x < ObjectMonitor::Knob_SpinLimit) { + if (x < Knob_Poverty) { + x = Knob_Poverty; + } + return x + Knob_Bonus; + } else { + return spin_duration; + } +} + +inline static int adjust_down(int spin_duration) { + // TODO: Use an AIMD-like policy to adjust _SpinDuration. + // AIMD is globally stable. + int x = spin_duration; + if (x > 0) { + // Consider an AIMD scheme like: x -= (x >> 3) + 100 + // This is globally sample and tends to damp the response. + x -= Knob_Penalty; + if (x < 0) { x = 0; } + return x; + } else { + return spin_duration; + } +} + +bool ObjectMonitor::short_fixed_spin(JavaThread* current, int spin_count, bool adapt) { + for (int ctr = 0; ctr < spin_count; ctr++) { + TryLockResult status = TryLock(current); + if (status == TryLockResult::Success) { + if (adapt) { + _SpinDuration = adjust_up(_SpinDuration); } - return 1; + return true; + } else if (status == TryLockResult::Interference) { + break; } SpinPause(); } + return false; +} + +// Spinning: Fixed frequency (100%), vary duration +bool ObjectMonitor::TrySpin(JavaThread* current) { + + // Dumb, brutal spin. Good for comparative measurements against adaptive spinning. + int knob_fixed_spin = Knob_FixedSpin; // 0 (don't spin: default), 2000 good test + if (knob_fixed_spin > 0) { + return short_fixed_spin(current, knob_fixed_spin, false); + } // Admission control - verify preconditions for spinning // @@ -1887,6 +1916,12 @@ int ObjectMonitor::TrySpin(JavaThread* current) { // becoming an absorbing state. Put another way, we spin briefly to // sample, just in case the system load, parallelism, contention, or lock // modality changed. + + int knob_pre_spin = Knob_PreSpin; // 10 (default), 100, 1000 or 2000 + if (short_fixed_spin(current, knob_pre_spin, true)) { + return true; + } + // // Consider the following alternative: // Periodically set _SpinDuration = _SpinLimit and try a long/full @@ -1895,8 +1930,8 @@ int ObjectMonitor::TrySpin(JavaThread* current) { // This takes us into the realm of 1-out-of-N spinning, where we // hold the duration constant but vary the frequency. - ctr = _SpinDuration; - if (ctr <= 0) return 0; + int ctr = _SpinDuration; + if (ctr <= 0) return false; // We're good to spin ... spin ingress. // CONSIDER: use Prefetch::write() to avoid RTS->RTO upgrades @@ -1928,7 +1963,7 @@ int ObjectMonitor::TrySpin(JavaThread* current) { // might update the poll values and we could be in a thread_blocked // state here which is not allowed so just check the poll. if (SafepointMechanism::local_poll_armed(current)) { - goto Abort; // abrupt spin egress + break; } SpinPause(); } @@ -1960,26 +1995,21 @@ int ObjectMonitor::TrySpin(JavaThread* current) { // If we acquired the lock early in the spin cycle it // makes sense to increase _SpinDuration proportionally. // Note that we don't clamp SpinDuration precisely at SpinLimit. - int x = _SpinDuration; - if (x < Knob_SpinLimit) { - if (x < Knob_Poverty) x = Knob_Poverty; - _SpinDuration = x + Knob_Bonus; - } - return 1; + _SpinDuration = adjust_up(_SpinDuration); + return true; } // The CAS failed ... we can take any of the following actions: // * penalize: ctr -= CASPenalty - // * exit spin with prejudice -- goto Abort; + // * exit spin with prejudice -- abort without adapting spinner // * exit spin without prejudice. // * Since CAS is high-latency, retry again immediately. - prv = ox; - goto Abort; + break; } // Did lock ownership change hands ? if (ox != prv && prv != nullptr) { - goto Abort; + break; } prv = ox; @@ -1989,20 +2019,10 @@ int ObjectMonitor::TrySpin(JavaThread* current) { } // Spin failed with prejudice -- reduce _SpinDuration. - // TODO: Use an AIMD-like policy to adjust _SpinDuration. - // AIMD is globally stable. - { - int x = _SpinDuration; - if (x > 0) { - // Consider an AIMD scheme like: x -= (x >> 3) + 100 - // This is globally sample and tends to damp the response. - x -= Knob_Penalty; - if (x < 0) x = 0; - _SpinDuration = x; - } + if (ctr < 0) { + _SpinDuration = adjust_down(_SpinDuration); } - Abort: if (_succ == current) { _succ = nullptr; // Invariant: after setting succ=null a contending thread @@ -2010,9 +2030,12 @@ int ObjectMonitor::TrySpin(JavaThread* current) { // in the normal usage of TrySpin(), but it's safest // to make TrySpin() as foolproof as possible. OrderAccess::fence(); - if (TryLock(current) > 0) return 1; + if (TryLock(current) == TryLockResult::Success) { + return true; + } } - return 0; + + return false; } diff --git a/src/hotspot/share/runtime/objectMonitor.hpp b/src/hotspot/share/runtime/objectMonitor.hpp index 6c0d5213605..fdc482a3555 100644 --- a/src/hotspot/share/runtime/objectMonitor.hpp +++ b/src/hotspot/share/runtime/objectMonitor.hpp @@ -353,8 +353,14 @@ private: void EnterI(JavaThread* current); void ReenterI(JavaThread* current, ObjectWaiter* current_node); void UnlinkAfterAcquire(JavaThread* current, ObjectWaiter* current_node); - int TryLock(JavaThread* current); - int TrySpin(JavaThread* current); + + + enum class TryLockResult { Interference = -1, HasOwner = 0, Success = 1 }; + + TryLockResult TryLock(JavaThread* current); + + bool TrySpin(JavaThread* current); + bool short_fixed_spin(JavaThread* current, int spin_count, bool adapt); void ExitEpilog(JavaThread* current, ObjectWaiter* Wakee); // Deflation support