070f6e47b3
-- 178e7a9a76fc8fcd6df6335b59139cbe644a16b9 by Jon Cohen <cohenjon@google.com>: Import of CCTZ from GitHub. PiperOrigin-RevId: 220523164 -- 59ef14fe7034a3148f1e9cef1f128b8ca264b444 by Jon Cohen <cohenjon@google.com>: Don't assume how much std::vector's constructors allocate in InlinedVector's test for scoped_allocator_adaptor support. PiperOrigin-RevId: 220464683 -- 6f8351be43a44a8f10bf20612b2cc744a4a911c7 by Jon Cohen <cohenjon@google.com>: Add VS Code and some Bazel output files to absl/.gitignore PiperOrigin-RevId: 220464362 -- 43fac22f8af6b6ed55309a784a9d25d837393d0e by Abseil Team <absl-team@google.com>: absl: fix SpinLock::EncodeWaitCycles If a thread has ever observed or set kSpinLockSleeper, it must never leave 0 in kWaitTimeMask because at this point it is expected to wake subsequent threads. Current calculations in EncodeWaitCycles can result in 0 in kWaitTimeMask and lead to missed wakeups. This is mostly theoretical today, because the futex call needs to finish within 128 cycles (futex can return immediately without waiting, but 128 cycles still look too low for this). But this can well fire in future if we bump granularity and/or threshold for recording contention. Use kSpinLockSleeper instead of 0. PiperOrigin-RevId: 220463123 -- def9b7e3d45c99d68cc52a4429256116d7f421f2 by Abseil Team <absl-team@google.com>: absl: optimize SpinLock::SlowLock Currently we record contention even after the first initial spin. This leads to several performance issues: 1. If we succeed in acquiring the lock after the initial spin, overall wait time can be within tens/hundreds of nanoseconds. Recording such low wait time looks completely unnecessary and excessive. From some point of view this is not even a wait, because we did not sleep. And, for example, Mutex does not record contention in this case. In majority of cases the lock should be acquired exactly during the initial spin, yet we still go through full overhead of submitting contention. 2. Whenever a thread submits contention it also calls FUTEX_WAKE (there is no way to understand if it's necessary or not when wait value is stored in the lock). So if there are just 2 threads and a brief contention, the second thread will still call FUTEX_WAKE which is completely unnecessary overhead. Don't record contention after the initial spin wait. FWIW this also removes 2 CycleClock::Now calls and EncodeWaitCycles from the common hot path. PiperOrigin-RevId: 220379972 -- 75b0c0cb214de904ea622f81ec3f4eabdc8695b0 by Derek Mauro <dmauro@google.com>: Supress MSVC warnings in raw_hash_set's use of TrailingZeros and LeadingZeros. https://github.com/abseil/abseil-cpp/issues/208 PiperOrigin-RevId: 220372204 GitOrigin-RevId: 178e7a9a76fc8fcd6df6335b59139cbe644a16b9 Change-Id: I3a66af4e050810a3274e45d4e055b2aa19ffba1b
231 lines
9.3 KiB
C++
231 lines
9.3 KiB
C++
// Copyright 2017 The Abseil Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "absl/base/internal/spinlock.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <limits>
|
|
|
|
#include "absl/base/attributes.h"
|
|
#include "absl/base/internal/atomic_hook.h"
|
|
#include "absl/base/internal/cycleclock.h"
|
|
#include "absl/base/internal/spinlock_wait.h"
|
|
#include "absl/base/internal/sysinfo.h" /* For NumCPUs() */
|
|
#include "absl/base/call_once.h"
|
|
|
|
// Description of lock-word:
|
|
// 31..00: [............................3][2][1][0]
|
|
//
|
|
// [0]: kSpinLockHeld
|
|
// [1]: kSpinLockCooperative
|
|
// [2]: kSpinLockDisabledScheduling
|
|
// [31..3]: ONLY kSpinLockSleeper OR
|
|
// Wait time in cycles >> PROFILE_TIMESTAMP_SHIFT
|
|
//
|
|
// Detailed descriptions:
|
|
//
|
|
// Bit [0]: The lock is considered held iff kSpinLockHeld is set.
|
|
//
|
|
// Bit [1]: Eligible waiters (e.g. Fibers) may co-operatively reschedule when
|
|
// contended iff kSpinLockCooperative is set.
|
|
//
|
|
// Bit [2]: This bit is exclusive from bit [1]. It is used only by a
|
|
// non-cooperative lock. When set, indicates that scheduling was
|
|
// successfully disabled when the lock was acquired. May be unset,
|
|
// even if non-cooperative, if a ThreadIdentity did not yet exist at
|
|
// time of acquisition.
|
|
//
|
|
// Bit [3]: If this is the only upper bit ([31..3]) set then this lock was
|
|
// acquired without contention, however, at least one waiter exists.
|
|
//
|
|
// Otherwise, bits [31..3] represent the time spent by the current lock
|
|
// holder to acquire the lock. There may be outstanding waiter(s).
|
|
|
|
namespace absl {
|
|
namespace base_internal {
|
|
|
|
ABSL_CONST_INIT static base_internal::AtomicHook<void (*)(const void *lock,
|
|
int64_t wait_cycles)>
|
|
submit_profile_data;
|
|
|
|
void RegisterSpinLockProfiler(void (*fn)(const void *contendedlock,
|
|
int64_t wait_cycles)) {
|
|
submit_profile_data.Store(fn);
|
|
}
|
|
|
|
// Uncommon constructors.
|
|
SpinLock::SpinLock(base_internal::SchedulingMode mode)
|
|
: lockword_(IsCooperative(mode) ? kSpinLockCooperative : 0) {
|
|
ABSL_TSAN_MUTEX_CREATE(this, __tsan_mutex_not_static);
|
|
}
|
|
|
|
SpinLock::SpinLock(base_internal::LinkerInitialized,
|
|
base_internal::SchedulingMode mode) {
|
|
ABSL_TSAN_MUTEX_CREATE(this, 0);
|
|
if (IsCooperative(mode)) {
|
|
InitLinkerInitializedAndCooperative();
|
|
}
|
|
// Otherwise, lockword_ is already initialized.
|
|
}
|
|
|
|
// Static (linker initialized) spinlocks always start life as functional
|
|
// non-cooperative locks. When their static constructor does run, it will call
|
|
// this initializer to augment the lockword with the cooperative bit. By
|
|
// actually taking the lock when we do this we avoid the need for an atomic
|
|
// operation in the regular unlock path.
|
|
//
|
|
// SlowLock() must be careful to re-test for this bit so that any outstanding
|
|
// waiters may be upgraded to cooperative status.
|
|
void SpinLock::InitLinkerInitializedAndCooperative() {
|
|
Lock();
|
|
lockword_.fetch_or(kSpinLockCooperative, std::memory_order_relaxed);
|
|
Unlock();
|
|
}
|
|
|
|
// Monitor the lock to see if its value changes within some time period
|
|
// (adaptive_spin_count loop iterations). The last value read from the lock
|
|
// is returned from the method.
|
|
uint32_t SpinLock::SpinLoop() {
|
|
// We are already in the slow path of SpinLock, initialize the
|
|
// adaptive_spin_count here.
|
|
ABSL_CONST_INIT static absl::once_flag init_adaptive_spin_count;
|
|
ABSL_CONST_INIT static int adaptive_spin_count = 0;
|
|
base_internal::LowLevelCallOnce(&init_adaptive_spin_count, []() {
|
|
adaptive_spin_count = base_internal::NumCPUs() > 1 ? 1000 : 1;
|
|
});
|
|
|
|
int c = adaptive_spin_count;
|
|
uint32_t lock_value;
|
|
do {
|
|
lock_value = lockword_.load(std::memory_order_relaxed);
|
|
} while ((lock_value & kSpinLockHeld) != 0 && --c > 0);
|
|
return lock_value;
|
|
}
|
|
|
|
void SpinLock::SlowLock() {
|
|
uint32_t lock_value = SpinLoop();
|
|
lock_value = TryLockInternal(lock_value, 0);
|
|
if ((lock_value & kSpinLockHeld) == 0) {
|
|
return;
|
|
}
|
|
// The lock was not obtained initially, so this thread needs to wait for
|
|
// it. Record the current timestamp in the local variable wait_start_time
|
|
// so the total wait time can be stored in the lockword once this thread
|
|
// obtains the lock.
|
|
int64_t wait_start_time = CycleClock::Now();
|
|
uint32_t wait_cycles = 0;
|
|
int lock_wait_call_count = 0;
|
|
while ((lock_value & kSpinLockHeld) != 0) {
|
|
// If the lock is currently held, but not marked as having a sleeper, mark
|
|
// it as having a sleeper.
|
|
if ((lock_value & kWaitTimeMask) == 0) {
|
|
// Here, just "mark" that the thread is going to sleep. Don't store the
|
|
// lock wait time in the lock as that will cause the current lock
|
|
// owner to think it experienced contention.
|
|
if (lockword_.compare_exchange_strong(
|
|
lock_value, lock_value | kSpinLockSleeper,
|
|
std::memory_order_relaxed, std::memory_order_relaxed)) {
|
|
// Successfully transitioned to kSpinLockSleeper. Pass
|
|
// kSpinLockSleeper to the SpinLockWait routine to properly indicate
|
|
// the last lock_value observed.
|
|
lock_value |= kSpinLockSleeper;
|
|
} else if ((lock_value & kSpinLockHeld) == 0) {
|
|
// Lock is free again, so try and acquire it before sleeping. The
|
|
// new lock state will be the number of cycles this thread waited if
|
|
// this thread obtains the lock.
|
|
lock_value = TryLockInternal(lock_value, wait_cycles);
|
|
continue; // Skip the delay at the end of the loop.
|
|
}
|
|
}
|
|
|
|
base_internal::SchedulingMode scheduling_mode;
|
|
if ((lock_value & kSpinLockCooperative) != 0) {
|
|
scheduling_mode = base_internal::SCHEDULE_COOPERATIVE_AND_KERNEL;
|
|
} else {
|
|
scheduling_mode = base_internal::SCHEDULE_KERNEL_ONLY;
|
|
}
|
|
// SpinLockDelay() calls into fiber scheduler, we need to see
|
|
// synchronization there to avoid false positives.
|
|
ABSL_TSAN_MUTEX_PRE_DIVERT(this, 0);
|
|
// Wait for an OS specific delay.
|
|
base_internal::SpinLockDelay(&lockword_, lock_value, ++lock_wait_call_count,
|
|
scheduling_mode);
|
|
ABSL_TSAN_MUTEX_POST_DIVERT(this, 0);
|
|
// Spin again after returning from the wait routine to give this thread
|
|
// some chance of obtaining the lock.
|
|
lock_value = SpinLoop();
|
|
wait_cycles = EncodeWaitCycles(wait_start_time, CycleClock::Now());
|
|
lock_value = TryLockInternal(lock_value, wait_cycles);
|
|
}
|
|
}
|
|
|
|
void SpinLock::SlowUnlock(uint32_t lock_value) {
|
|
base_internal::SpinLockWake(&lockword_,
|
|
false); // wake waiter if necessary
|
|
|
|
// If our acquisition was contended, collect contentionz profile info. We
|
|
// reserve a unitary wait time to represent that a waiter exists without our
|
|
// own acquisition having been contended.
|
|
if ((lock_value & kWaitTimeMask) != kSpinLockSleeper) {
|
|
const uint64_t wait_cycles = DecodeWaitCycles(lock_value);
|
|
ABSL_TSAN_MUTEX_PRE_DIVERT(this, 0);
|
|
submit_profile_data(this, wait_cycles);
|
|
ABSL_TSAN_MUTEX_POST_DIVERT(this, 0);
|
|
}
|
|
}
|
|
|
|
// We use the upper 29 bits of the lock word to store the time spent waiting to
|
|
// acquire this lock. This is reported by contentionz profiling. Since the
|
|
// lower bits of the cycle counter wrap very quickly on high-frequency
|
|
// processors we divide to reduce the granularity to 2^PROFILE_TIMESTAMP_SHIFT
|
|
// sized units. On a 4Ghz machine this will lose track of wait times greater
|
|
// than (2^29/4 Ghz)*128 =~ 17.2 seconds. Such waits should be extremely rare.
|
|
enum { PROFILE_TIMESTAMP_SHIFT = 7 };
|
|
enum { LOCKWORD_RESERVED_SHIFT = 3 }; // We currently reserve the lower 3 bits.
|
|
|
|
uint32_t SpinLock::EncodeWaitCycles(int64_t wait_start_time,
|
|
int64_t wait_end_time) {
|
|
static const int64_t kMaxWaitTime =
|
|
std::numeric_limits<uint32_t>::max() >> LOCKWORD_RESERVED_SHIFT;
|
|
int64_t scaled_wait_time =
|
|
(wait_end_time - wait_start_time) >> PROFILE_TIMESTAMP_SHIFT;
|
|
|
|
// Return a representation of the time spent waiting that can be stored in
|
|
// the lock word's upper bits.
|
|
uint32_t clamped = static_cast<uint32_t>(
|
|
std::min(scaled_wait_time, kMaxWaitTime) << LOCKWORD_RESERVED_SHIFT);
|
|
|
|
if (clamped == 0) {
|
|
return kSpinLockSleeper; // Just wake waiters, but don't record contention.
|
|
}
|
|
// Bump up value if necessary to avoid returning kSpinLockSleeper.
|
|
const uint32_t kMinWaitTime =
|
|
kSpinLockSleeper + (1 << LOCKWORD_RESERVED_SHIFT);
|
|
if (clamped == kSpinLockSleeper) {
|
|
return kMinWaitTime;
|
|
}
|
|
return clamped;
|
|
}
|
|
|
|
uint64_t SpinLock::DecodeWaitCycles(uint32_t lock_value) {
|
|
// Cast to uint32_t first to ensure bits [63:32] are cleared.
|
|
const uint64_t scaled_wait_time =
|
|
static_cast<uint32_t>(lock_value & kWaitTimeMask);
|
|
return scaled_wait_time
|
|
<< (PROFILE_TIMESTAMP_SHIFT - LOCKWORD_RESERVED_SHIFT);
|
|
}
|
|
|
|
} // namespace base_internal
|
|
} // namespace absl
|