All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.tauri.choam.internal.mcas.RefIdGen.scala Maven / Gradle / Ivy

There is a newer version: 0.4.10-3-3a25cbb
Show newest version
/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2016-2024 Daniel Urban and contributors listed in NOTICE.txt
 *
 * 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.
 */

package dev.tauri.choam
package internal
package mcas

import RefIdGenBase.GAMMA

/**
 * `RefIdGen` generates `Long` IDs for `Ref`s in a way which
 * guarantees uniqueness (which is a must for `Ref`s) and
 * scales with the number of cores (so that ID generation is
 * not a bottleneck for `Rxn`s which allocate a lot of `Ref`s).
 *
 * The idea is that every thread has its own `ThreadLocalRefIdGen`,
 * which has a "preallocated block" of IDs. As long as possible,
 * it returns IDs from that block. This should be very fast,
 * because it is entirely thread-local. When the block is
 * exhausted, it requests a new block from the global (shared)
 * `RefIdGen`.
 *
 * The sizing of these thread-local blocks should take into
 * account performance: big blocks are good, because it means
 * that we're mostly working thread-locally. However, if a
 * thread is abandoned, its remaining preallocated IDs in its
 * current block are "wasted" or "leaked". If we waste too much,
 * we could overflow the global `ctr`. (Abandoning "real" (i.e.,
 * platform) threads is probably not a concern, however on
 * newer JVMs we also have virtual threads. These are designed
 * to be used shortly, then be abandoned. And they're very
 * fast to create.) So very big blocks are not good.
 *
 * So, to avoid overflow, every `ThreadLocalRefIdGen` starts from
 * a small block, and doubles the size with every new block
 * (up to a limit). This way every thread only wastes at most
 * half of the IDs it preallocates. So even in the worst
 * case (if we create a lot of virtual threads, and each
 * of them is abandoned at the worst possible time) we only
 * waste half of the IDs. That means we still have 63 bits,
 * (so half of the whole ID space) which should be plenty.
 * (The doubling also means, that threads which create a lot
 * of `Ref`s quickly get up to big blocks. Also, allocating
 * prematurely new blocks for `Ref.Array`s doesn't mess up
 * this, see comment in `nextArrayIdBase`.)
 *
 * Generated IDs should not be contiguous, because we'd like
 * to put them into a hash-map, hash-trie or similar. (Of course
 * we could hash them later, but we'd rather do this once on
 * generation.) So we use Fibonacci hashing to generate a
 * "good" distribution.
 */
private[choam] sealed trait RefIdGen {
  def nextId(): Long
  def nextArrayIdBase(size: Int): Long
}

private[choam] object RefIdGen {

  val global: GlobalRefIdGen = // TODO: instead of this, have a proper acq/rel Runtime
    new GlobalRefIdGen

  /** The computed ID must've been already allocated in a block! */
  final def compute(base: Long, offset: Int): Long = {
    (base + offset.toLong) * GAMMA
  }
}

private[choam] final class GlobalRefIdGen private[mcas] () extends RefIdGenBase with RefIdGen {

  private[this] final val initialBlockSize =
    2 // TODO: maybe start with bigger for platform threads?

  private[GlobalRefIdGen] final def allocateThreadLocalBlock(size: Int): Long = {
    require(size > 0)
    val s = size.toLong
    val n = this.getAndAddCtrO(s)
    require(n < (n + s)) // ID overflow
    n
  }

  final def newThreadLocal(): RefIdGen = {
    new GlobalRefIdGen.ThreadLocalRefIdGen(
      parent = this,
      next = 0L, // unused, because:
      remaining = 0, // initially no more remaining
      nextBlockSize = initialBlockSize,
    )
  }

  @inline
  final override def nextId(): Long =
    this.nextIdGlobal()

  @inline
  final override def nextArrayIdBase(size: Int): Long =
    this.nextArrayIdBaseGlobal(size)

  /**
   * Slower fallback to still be able to generate
   * an ID when we don't have access to a thread-
   * local context.
   */
  final def nextIdGlobal(): Long = {
    val n = this.getAndAddCtrO(1L)
    require(n < (n + 1L)) // ID overflow
    n * GAMMA
  }

  /** Returns idBase for RefArrays */ // TODO: is ID overflow plausible with big arrays?
  final def nextArrayIdBaseGlobal(size: Int): Long = {
    this.allocateThreadLocalBlock(size)
  }
}

private[mcas] object GlobalRefIdGen {

  final class ThreadLocalRefIdGen private[GlobalRefIdGen] (
    private[this] val parent: GlobalRefIdGen,
    private[this] var next: Long,
    private[this] var remaining: Int,
    private[this] var nextBlockSize: Int,
  ) extends RefIdGen {

    private[this] final val maxBlockSize =
      1 << 30

    @tailrec
    final override def nextId(): Long = {
      val rem = this.remaining
      if (rem > 0) {
        val n = this.next
        this.next = n + 1L
        this.remaining = rem - 1
        n * GAMMA
      } else {
        val s = this.nextBlockSize
        this.next = this.parent.allocateThreadLocalBlock(s)
        this.remaining = s
        if (s < maxBlockSize) {
          this.nextBlockSize = s << 1
        }
        // now we'll succeed for sure:
        this.nextId()
      }
    }

    @tailrec
    final override def nextArrayIdBase(size: Int): Long = {
      require(size > 0)
      val rem = this.remaining
      if (rem >= size) {
        // It fits into the current block:
        val base = this.next
        this.next = base + size.toLong
        this.remaining = rem - size
        base
      } else if (size <= maxBlockSize) {
        // It doesn't fit into the current block, but
        // it can fit into a new block. We'll leak the
        // remaining IDs in the current block, but also
        // use at least one more than that from the next
        // block, so overall we're fine.
        this.allocateBlockForArray(size)
        // now we'll succeed for sure:
        this.nextArrayIdBase(size)
      } else {
        // Exceptionally large array, so we just fulfill
        // this request directly from the global. (There
        // is zero leak here, but also no thread-local
        // optimization.)
        this.parent.nextArrayIdBaseGlobal(size)
      }
    }

    private[this] final def allocateBlockForArray(arraySize: Int): Unit = {
      val abs = RefIdGenBase.nextPowerOf2(arraySize)
      val nbs = this.nextBlockSize
      val s = java.lang.Math.max(abs, nbs)
      this.next = this.parent.allocateThreadLocalBlock(s)
      this.remaining = s
      if (s < maxBlockSize) {
        this.nextBlockSize = s << 1
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy