Skip to content

Make the segment pool lock-free on the JVM#717

Merged
swankjesse merged 1 commit intomasterfrom
jwilson.0523.lock_free
May 25, 2020
Merged

Make the segment pool lock-free on the JVM#717
swankjesse merged 1 commit intomasterfrom
jwilson.0523.lock_free

Conversation

@swankjesse
Copy link
Collaborator

Under a high contention environment we'll do more zero-fill and GC
instead of locking.

@swankjesse swankjesse mentioned this pull request May 23, 2020
Copy link
Contributor

@Egorand Egorand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have the new implementation in commonMain? AtomicLong and AtomicReference are available in kotlin-stdlib / kotlin.native.concurrent.


actual fun take(): Segment {
return when (val first = firstRef.getAndSet(LOCK)) {
LOCK -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if when will do a ref check or an equality check here? Will this code break if it's an equality check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your idea of using ===. Lemme switch to that. (They’ll do the same thing regardless, because Segment doesn’t override equals()).

}

actual fun recycle(segment: Segment) {
require(segment.next == null && segment.prev == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe add an error message? Or even split this into two requires?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s more of an assert than anything. This isn’t public API. I’d prefer to not split it up because its primary purpose is documentation.

if (atomicByteCount.get() >= MAX_SIZE) return // Pool is full.

val first = firstRef.get()
if (first == LOCK) return // A take() is currently in progress.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first === LOCK?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fixed.

Copy link
Collaborator

@JakeWharton JakeWharton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see this rolled out to a highly concurrent server as an experiment before we ship it

/** Total bytes in this pool. */
private var atomicByteCount = AtomicLong()

actual val byteCount: Long
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this used for? The order of operations means that you can observe this being larger than what's actually available.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just testing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lemme document that.

if (atomicByteCount.get() >= MAX_SIZE) return // Pool is full.

val first = firstRef.get()
if (first == LOCK) return // A take() is currently in progress.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

===

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


actual fun take(): Segment {
return when (val first = firstRef.getAndSet(LOCK)) {
LOCK -> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will do == but we probably want ===

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good call!

internal actual object SegmentPool {
/** The maximum number of bytes to pool. */
// TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
actual val MAX_SIZE = 64 * 1024L // 64 KiB.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

actual val MAX_SIZE = 64 * 1024L // 64 KiB.

/** A sentinel segment to indicate that the linked list is currently being modified. */
private val LOCK = Segment(ByteArray(0), pos = 0, limit = 0, shared = false, owner = false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it’s an object I get that for free.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, only the fields, not the methods!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed all to be @JvmStatic. Can’t hurt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why care about @JvmStatic on private fields though? To avoid calling getters internally? Won't it be @JvmField then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to go with @JvmStatic on the methods and not on the fields. That’s the least amount of code and the simplest dispatch for callers.

private val LOCK = Segment(ByteArray(0), pos = 0, limit = 0, shared = false, owner = false)

/** Singly-linked list of segments. */
private var firstRef = AtomicReference<Segment>()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

private var firstRef = AtomicReference<Segment>()

/** Total bytes in this pool. */
private var atomicByteCount = AtomicLong()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

/** Total bytes in this pool. */
private var atomicByteCount = AtomicLong()

actual val byteCount: Long
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

actual val byteCount: Long
get() = atomicByteCount.get()

actual fun take(): Segment {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

}
}

actual fun recycle(segment: Segment) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JvmStatic

@swankjesse
Copy link
Collaborator Author

Agreed 100% on wanting to give this lots of exercise before shipping it to everyone!

@swankjesse swankjesse force-pushed the jwilson.0523.lock_free branch from 884f38f to 52859e5 Compare May 24, 2020 01:33
@swankjesse
Copy link
Collaborator Author

@Egorand I can’t do commonMain ’cause JVM/AtomicReference and Kotlin/AtomicReference don’t have an expect definition in common. When I tried to hack one myself, KMP got angry at the type parameters on AtomicReference.

@swankjesse
Copy link
Collaborator Author

This branch released as 2.7.0-alpha.lockfree.1.

Copy link
Collaborator

@bnorm bnorm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are aware of https://github.com/Kotlin/kotlinx.atomicfu? Used by kotlinx-coroutines for atomics in common code. Compile time only as well. Could help remove the expect/actual if you wanted to go down that route.


/** Singly-linked list of segments. */
@JvmStatic
private var firstRef = AtomicReference<Segment>()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicReference<Segment?> will get you some null type safety. Otherwise firstRef.getAndSet(LOCK) and firstRef.get() will return platform types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh I like it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(done)

Under a high contention environment we'll do more zero-fill and GC
instead of locking.
@swankjesse swankjesse force-pushed the jwilson.0523.lock_free branch from 52859e5 to fae6650 Compare May 25, 2020 13:18
@swankjesse
Copy link
Collaborator Author

AtomicFU looks brilliant. That’d be an excellent tool for our toolchain if/when we get serious about OkHttp in Kotlin/Native. For now I’m reluctant only because it’s experimental magic, and Okio is no place for experimental magic.

@codefromthecrypt
Copy link

added some notes after reviewing this incredibly late #831

val first = firstRef.get()
if (first === LOCK) return // A take() is currently in progress.

segment.next = first

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry if I'm showing my lack of kotlin knowledge, but I'm wondering how these 3 changes will be guaranteed to be visible in the right order to other threads? We aren't using locks or volatile inside Segment as far as I can tell.. what am I missing? (I want to follow up with a comment to explain this later unless it is generally understood kotlin thing)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right. below we are using compareAndSet, not weakCompareAndSet, and that is what's allowing these changes to be safely published to the consumer of the pool (happens-before relationship) https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants