Skip to main content

Getting things out

The exits are boring, as they should be: turning an FArray back into an array, a string, or a standard-library Seq (Scala's read-only sequence interface, the role java.util.List plays in Java) is either a raw copy or no copy at all. There's exactly one interesting decision here, how toSeq provides the collections interface without materializing anything, and it's the reason the conversions are cheap.

The copies

toArray, copyToArray, and indices are memory-bound and tie the field: there's no cleverness available, everyone does the same arraycopy for the same n elements. The one thing FArray can get wrong here is the type of the copy: a reference FArray backed by Object[] makes toArray[String] a checked per-element copy rather than a bulk arraycopy. Whether you dodge that depends on how the FArray was born: tabulate/fill (with a ClassTag in scope, Scala's runtime class token that survives erasure) and fromArray of a typed array give a real String[] backing, so toArray is a straight arraycopy; a result of map/filter/collect is Object[]-backed and pays the checked copy. It's an island, not a guarantee, worth knowing because it's easy to assume it's always fast.

mkString is the one exit that pulls ahead: a tuned append loop over the backing array runs ~1.3–1.5× past the rivals at scale (and lopsidedly more at tiny sizes, where their builder setup dominates).

measuring…

toSeq: the interface without the collection

The interesting exit is the one that copies nothing. List and Vector are Seqs, so their toSeq returns this for free, and an FArray that had to materialize a fresh Seq would lose that comparison by a whole allocation on every call. FArray materializes nothing.

toSeq and toIndexedSeq return an FArraySeq, a tiny wrapper that is an immutable.IndexedSeq and holds the original FArray by reference:

final class FArraySeq[A](under: FBase) extends immutable.IndexedSeq[A]:
def apply(i: Int): A = under.applyBoxed(i).asInstanceOf[A]
def length: Int = under.length
override def knownSize: Int = under.length

That's the whole thing: O(1) to construct, no elements copied, and now you have the full standard-library Seq API (pattern matching, Scala's for-comprehension loops, anything typed to Seq[A]) over your FArray's storage. The cost is in the signature: a Seq[A] hands back boxed A, so reads through the wrapper go via applyBoxed and a primitive re-boxes on the way out. The wrapper is for handing an FArray to code that speaks Seq; unboxed reads stay on the FArray surface, which is the entire rest of this section.

The trade: FArray pays one small wrapper allocation where List/Vector pay zero (they were already Seqs). That single allocation is the one un-winnable cell against a native Seq, and the benchmark suite omits it rather than dress it up: a loss of exactly one object, in exchange for having a real array underneath everywhere else.