Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize spin loops and fix regression #298

Merged
merged 13 commits into from
Apr 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ package org.jetbrains.kotlinx.lincheck.runner
import kotlinx.atomicfu.*
import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.util.Spinner
import org.jetbrains.kotlinx.lincheck.util.SpinnerGroup
import org.jetbrains.kotlinx.lincheck.util.spinWaitBoundedFor
import org.jetbrains.kotlinx.lincheck.util.*
import org.jetbrains.kotlinx.lincheck.TestThread
import java.io.*
import java.lang.*
Expand Down Expand Up @@ -50,7 +48,12 @@ internal class FixedActiveThreadsExecutor(private val testName: String, private
*
* Only the main thread submitting tasks manipulates this spinner.
*/
private val resultSpinner = Spinner(nThreads)
// we set `nThreads + 1` as a number of threads, because
// we have `nThreads` of the scenario plus the main thread waiting for the result;
// if this number is greater than the number of available CPUs,
// the main thread will be parked immediately without spinning;
// in this case, if `nCPUs = nThreads` all the scenario threads still will be spinning
private val resultSpinner = Spinner(nThreads + 1)

/**
* This flag is set to `true` when [await] detects a hang.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import org.jetbrains.kotlinx.lincheck.runner.ParallelThreadsRunner.Completion.*
import org.jetbrains.kotlinx.lincheck.runner.UseClocks.*
import org.jetbrains.kotlinx.lincheck.strategy.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy
import org.jetbrains.kotlinx.lincheck.util.SpinnerGroup
import org.jetbrains.kotlinx.lincheck.util.spinWaitUntil
import org.jetbrains.kotlinx.lincheck.util.*
import org.jetbrains.kotlinx.lincheck.TestThread
import java.lang.reflect.*
import java.util.concurrent.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart.*
import org.jetbrains.kotlinx.lincheck.strategy.*
import org.jetbrains.kotlinx.lincheck.util.SpinnerGroup
import org.jetbrains.kotlinx.lincheck.util.spinWaitUntil
import org.jetbrains.kotlinx.lincheck.util.*
import org.jetbrains.kotlinx.lincheck.verifier.*
import org.jetbrains.kotlinx.lincheck.transformation.CodeLocations
import org.jetbrains.kotlinx.lincheck.Injections
Expand Down
253 changes: 57 additions & 196 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/util/Spinner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,80 +11,16 @@
package org.jetbrains.kotlinx.lincheck.util

/**
* A spinner implements spinning in a loop with optional yielding to other threads.
*
* This class provides a method [spin], that should be called inside a spin-loop.
* This method performs a few spin-loop iterations and optionally periodically yields.
*
* For example, a simple spin-lock class can be implemented with the help of the [Spinner] as follows:
*
* ```
* class SpinLock {
* private val lock = AtomicBoolean()
* private val spinner = Spinner()
*
* fun lock() {
* while (!lock.compareAndSet(false, true)) {
* spinner.spin()
* }
* }
*
* fun unlock() {
* lock.set(false)
* }
* }
* ```
*
* The `lock` method can be shortened with the help of the [spinWaitUntil] extension method:
*
* ```
* fun lock() {
* spinner.spinWaitUntil { lock.compareAndSet(false, true) }
* }
* ```
*
* Sometimes, it is useful to fall back into a blocking synchronization if the spin-loop spins for too long.
* For this purpose, the [spin] method has a boolean return value.
* It returns `true` if the spinning should be continued,
* or `false` if it is advised to exit the spin-loop.
*
* ```
* class SimpleQueuedLock {
* private val lock = AtomicBoolean()
* private val queue = ConcurrentLinkedQueue<Thread>()
* private val spinner = Spinner()
*
* fun lock() {
* while (!lock.compareAndSet(false, true)) {
* if (spinner.spin())
* continue
* val thread = Thread.currentThread()
* if (!queue.contains(thread))
* queue.add(thread)
* LockSupport.park()
* }
* }
*
* fun unlock() {
* lock.set(false)
* queue.poll()?.also {
* LockSupport.unpark(it)
* }
* }
* }
* ```
* A spinner implements utility functions for spinning in a loop.
*
* @property nThreads If passed, denotes the number of threads in a group that
* may wait for a common condition in the spin-loop.
* This information is used to check if the number of available CPUs is greater than
* the number of threads, and avoid spinning if that is not the case.
*
* @constructor Creates an instance of the [Spinner] class.
*/
class Spinner(val nThreads: Int = -1) {

/**
* Counter of performed spin-loop iterations.
*/
private var counter: Int = 0
internal class Spinner(val nThreads: Int = -1) {

/**
* Determines whether the spinner should actually spin in a loop,
Expand All @@ -95,154 +31,69 @@ class Spinner(val nThreads: Int = -1) {
* If the number of processors is less than the number of threads,
* then the spinner should exit the loop immediately.
*/
private val shouldSpin: Boolean = run {
val shouldSpin: Boolean = run {
val nProcessors = Runtime.getRuntime().availableProcessors()
(nProcessors >= nThreads)
}

/**
* The number of spin-loop iterations to be performed per call to [spin].
*/
private val spinLoopIterationsPerCall: Int =
if (shouldSpin) SPIN_LOOP_ITERATIONS_PER_CALL else 1

/**
* The number of spin-loop iterations before yielding the current thread
* to give other threads the opportunity to run.
*/
private val yieldLimit =
if (shouldSpin) SPIN_LOOP_ITERATIONS_BEFORE_YIELD else 1

/**
* The exit limit determines the number of spin-loop iterations
* after which the spin-loop is advised to exit.
*/
private val exitLimit =
if (shouldSpin) SPIN_LOOP_ITERATIONS_BEFORE_EXIT else 1

/**
* Spins the counter for a few iterations.
* In addition, in the case of a yielding spinner, it yields occasionally
* to give other threads the opportunity to run.
* Waits in the spin-loop until the given condition is true
* with periodical yielding to other threads.
*
* @return `true` if the spin-loop should continue;
* `false` if the spin-loop is advised to exit,
* for example, to fall back into a blocking synchronization.
* @param condition A lambda function that determines the condition to wait for.
* The function should return true when the condition is satisfied, and false otherwise.
*/
fun spin(): Boolean {
// perform spin waiting
Thread.onSpinWait()
spinWait()
// update the counter
counter += spinLoopIterationsPerCall
// if yield limit is approached,
// then yield and give other threads the opportunity to run
if (counter % yieldLimit == 0) {
Thread.yield()
inline fun spinWaitUntil(condition: () -> Boolean) {
var counter = 0
val yieldLimit = 1 + if (shouldSpin) SPIN_CYCLES_BOUND else 0
while (!condition()) {
Thread.onSpinWait()
counter++
if (counter % yieldLimit == 0) {
Thread.yield()
}
}
// if exit limit is approached,
// reset counter and signal to exit the spin-loop
if (counter >= exitLimit) {
counter = 0
return false
}
return true
}

/**
* Auxiliary variable storing a pseudo-random value,
* which is used inside the [spinWait] to perform spin waiting.
*/
private var sink: Long = System.nanoTime()

/**
* Implements a spin waiting procedure.
* Waits in the spin-loop until the given condition is true.
* Exits the spin-loop after a certain number of spin-loop iterations ---
* typically, in this case, one may want to fall back into some blocking synchronization.
*
* @param condition A lambda function that determines the condition to wait for.
* The function should return true when the condition is satisfied, and false otherwise.
*
* @return `true` if the condition is met; `false` if the condition was not met and
* the spin-wait loop exited because the bound was reached.
*/
private fun spinWait() {
// Initialize with a pseudo-random number to prevent optimizations.
var x = sink
// We want to perform few spins while avoiding accesses to the shared memory.
// To achieve this, we do some arithmetic operations on a local variable
// and try to obfuscate the loop body so that the compiler
// would not be able to optimize it out.
for (i in spinLoopIterationsPerCall downTo 1) {
x += (31 * x + 0xBEEF + i) and (0xFFFFFFFFFFFFFFFL)
inline fun Spinner.spinWaitBoundedUntil(condition: () -> Boolean): Boolean {
var counter = 0
val exitLimit = if (shouldSpin) SPIN_CYCLES_BOUND else 0
var result = true
while (!condition()) {
if (counter == exitLimit) {
result = condition()
break
}
Thread.onSpinWait()
counter++
}
// This if statement ensures that the result of the computation
// will have a visible side effect and thus will not be optimized,
// but at the same time it avoids the actual store on a hot-path.
if (x == 0xDEADL) {
sink += x
}
}

/**
* Resets the state of the spinner.
*/
fun reset() {
counter = 0
}

}

/**
* A [SpinnerGroup] function creates a list of spinners to be used by the specified number of threads.
* It provides a convenient way to manage multiple spinners together.
*
* @param nThreads The number of threads in the group.
*/
fun SpinnerGroup(nThreads: Int): List<Spinner> {
return Array(nThreads) { Spinner(nThreads) }.asList()
}

/**
* Waits in the spin-loop until the given condition is true.
*
* @param condition a lambda function that determines the condition to wait for.
* The function should return true when the condition is satisfied, and false otherwise.
*
* @see Spinner
*/
inline fun Spinner.spinWaitUntil(condition: () -> Boolean) {
while (!condition()) {
spin()
return result
}
reset()
}

/**
* Waits in the spin-loop until the given condition is true.
* Exits the spin-loop after a certain number of spin-loop iterations.
*
* @param condition a lambda function that determines the condition to wait for.
* The function should return true when the condition is satisfied, and false otherwise.

* @return `true` if the condition is met; `false` if the condition is not met.
*
* @see Spinner
*/
inline fun Spinner.spinWaitBoundedUntil(condition: () -> Boolean): Boolean {
var result = true
while (!condition()) {
if (spin()) continue
result = condition()
break
}
reset()
return result
}

/**
* Waits for the result of the given [getter] function in the spin-loop until the result is not null.
* Exits the spin-loop after a certain number of spin-loop iterations.
*
* @param getter a lambda function that returns the result to wait for.
* @param getter A lambda function that returns the result to wait for.
*
* @return the result of waiting.
* @return The result of waiting, or null if
* the spin-wait loop exited because the bound was reached.
*
* @see Spinner
* @see Spinner.spinWaitBoundedFor
*/
inline fun <T> Spinner.spinWaitBoundedFor(getter: () -> T?): T? {
internal inline fun <T> Spinner.spinWaitBoundedFor(getter: () -> T?): T? {
spinWaitBoundedUntil {
val result = getter()
if (result != null)
Expand All @@ -252,6 +103,16 @@ inline fun <T> Spinner.spinWaitBoundedFor(getter: () -> T?): T? {
return null
}

private const val SPIN_LOOP_ITERATIONS_PER_CALL : Int = 100
private const val SPIN_LOOP_ITERATIONS_BEFORE_YIELD : Int = 10_000_000
private const val SPIN_LOOP_ITERATIONS_BEFORE_EXIT : Int = 100_000_000
/**
* A [SpinnerGroup] function creates a list of spinners to be used by the specified number of threads.
* It provides a convenient way to manage multiple spinners together.
*
* @param nThreads The number of threads in the group.
*/
@Suppress("FunctionName")
internal fun SpinnerGroup(nThreads: Int): List<Spinner> {
return Array(nThreads) { Spinner(nThreads) }.asList()
}


const val SPIN_CYCLES_BOUND: Int = 1_000_000