8000 Compiler performance improvements around Symbols and Types by retronym · Pull Request #5823 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

Compiler performance improvements around Symbols and Types #5823

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

Merged
merged 16 commits into from
May 24, 2017

Conversation

retronym
Copy link
Member
@retronym retronym commented Apr 4, 2017

Seems to bring about 4% in aggregate.

Thematically cherry-picked from #5785

UPDATE : benchmark results, with the latest iteration of this PR.

Before:

$ for sha in 2.12.3-bin-9acab45-SNAPSHOT 2.12.3-bin-9b28b01-SNAPSHOT ; do sbt "set scalaVersion in compilation := \"$sha\"" "hot -psource= -f3 -wi 10 -jvmArgs -Xmx1G"; sleep 10; done

[info] Benchmark                                   (extraArgs)  (source)    Mode  Cnt     Score   Error  Units
[info] HotScalacBenchmark.compile                                         sample  240  1299.221 ± 6.034  ms/op
[info] HotScalacBenchmark.compile:compile·p0.00                           sample       1270.874          ms/op
[info] HotScalacBenchmark.compile:compile·p0.50                           sample       1291.846          ms/op
[info] HotScalacBenchmark.compile:compile·p0.90                           sample       1319.109          ms/op
[info] HotScalacBenchmark.compile:compile·p0.95                           sample       1354.655          ms/op
[info] HotScalacBenchmark.compile:compile·p0.99                           sample       1433.592          ms/op
[info] HotScalacBenchmark.compile:compile·p0.999                          sample       1434.452          ms/op
[info] HotScalacBenchmark.compile:compile·p0.9999                         sample       1434.452          ms/op
[info] HotScalacBenchmark.compile:compile·p1.00                           sample       1434.452          ms/op

After:

[info] Benchmark                                   (extraArgs)  (source)    Mode  Cnt     Score   Error  Units
[info] HotScalacBenchmark.compile                                         sample  251  1251.348 ± 7.449  ms/op
[info] HotScalacBenchmark.compile:compile·p0.00                           sample       1214.251          ms/op
[info] HotScalacBenchmark.compile:compile·p0.50                           sample       1243.611          ms/op
[info] HotScalacBenchmark.compile:compile·p0.90                           sample       1277.166          ms/op
[info] HotScalacBenchmark.compile:compile·p0.95                           sample       1335.466          ms/op
[info] HotScalacBenchmark.compile:compile·p0.99                           sample       1418.933          ms/op
[info] HotScalacBenchmark.compile:compile·p0.999                          sample       1451.229          ms/op
[info] HotScalacBenchmark.compile:compile·p0.9999                         sample       1451.229          ms/op
[info] HotScalacBenchmark.compile:compile·p1.00                           sample       1451.229          ms/op

@scala-jenkins scala-jenkins added this to the 2.12.3 milestone Apr 4, 2017
@retronym retronym requested a review from adriaanm April 4, 2017 01:08
@@ -696,21 +696,23 @@ trait Types
* }}}
*/
def memberInfo(sym: Symbol): Type = {
// assert(sym ne NoSymbol, this)
require(sym ne NoSymbol, this)
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems to be more than an optimization
@adriaanm: l0rinc@1f6f7f8#diff-ecaa4198a04261cd0e00a708b695eafbL689

*/
override def boundSyms: Set[Symbol] = emptySymbolSet

/*
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: you may want to delete commented code instead of formatting it

(thiss eq that) || {
var these = thiss
var those = that
while (!these.isEmpty && !those.isEmpty && these.head.equals(those.head)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: could probably be while (these.nonEmpty && those.nonEmpty

Copy link
Member Author

Choose a reason for hiding this comment

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

I've got a bit of an aversion to nonEmpty (a mixed in trait method) in hot code because in some cases it is harder for JIT to devirtualize and inline. Not sure whether that's the case here.

@@ -61,6 +61,19 @@ trait Collections {
head
}

final def sameElementsEquals(thiss: List[AnyRef], that: List[AnyRef]): Boolean = {
// Probably immutable, so check reference identity first (it's quick anyway)
(thiss eq that) || {
Copy link
Contributor

Choose a reason for hiding this comment

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

question: this inlining of thiss.sameElements(that) seems like a micro-optimization to my untrained eye :)

Copy link
Member Author

Choose a reason for hiding this comment

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

The key difference is using .equals rather than .==. The latter is routed through BoxesRuntime.equals which needs to perform a few type tests on the operands before delegating to equals in order to paper over the differences between primitive and boxed numeric equality.

finalizeHash(mix(h, args.hashCode()), 3)
else
finalizeHash(h, 2)
var i = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

could be inited with 2 to avoid 2 + i later

@@ -184,8 +184,8 @@ private[internal] trait GlbLubs {
/** Eliminate from list of types all elements which are a supertype
* of some other element of the list. */
private def elimSuper(ts: List[Type]): List[Type] = ts match {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't claim to understand it, but I have a question:
This seems to be an O(n^2) operation (i.e. for every element search all remaining). Would it be faster to cache the parents in a Set and check the set for inconsistencies for the previously encountered parents instead?
That could make it linear, I think.
(would probably be slower if the number of elements is small)

Copy link
Member Author

Choose a reason for hiding this comment

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

Type-s aren't amenable to use in a HashSet because =:= isn't compatible with hashCode. This code here is actually using subtyping, which makes it harder again to find ways to beat O(N^2).

@@ -195,8 +195,8 @@ private[internal] trait GlbLubs {
* of some other element of the list. */
private def elimSub(ts: List[Type], depth: Depth): List[Type] = {
def elimSub0(ts: List[Type]): List[Type] = ts match {
case List() => List()
case List(t) => List(t)
case List() => ts
Copy link
Contributor

Choose a reason for hiding this comment

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

could probably be:

case List() | List(_) => ts

instead

mapOverArgs(args, sym.typeParams)
else
if (trackVariance && args.nonEmpty && !variance.isInvariant) {
val tparams = sym.typeParams
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: to make it more obvious what happened to the sym.typeParams.nonEmpty part of the if, this could probably be simplified to

if (sym.typeParams.nonEmpty) mapOverArgs(args, sym.typeParams)
else args mapConserve this

@@ -722,9 +722,20 @@ private[internal] trait TypeMaps {
else subst(tp, sym, from.tail, to.tail)
)

private def fromContains(syms: Set[Symbol]): Boolean = {
if (!syms.isEmpty) {
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: can probably be syms.nonEmpty && {

case Nil => Nil
@tailrec
def loop(xs: List[Symbol], result: List[Symbol]): List[Symbol] = xs match {
case Nil => result
case x :: xs =>
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: could also be y :: ys to avoid name collision with parent match

@xeno-by xeno-by changed the title Compiler performance improvments around Symbols and Types Compiler performance improvements around Symbols and Types Apr 4, 2017
Copy link
Member
@lrytz lrytz left a comment

Choose a reason for hiding this comment

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

Looking good!

@@ -3604,7 +3604,10 @@ trait Types
if (sym.isAliasType && sameLength(sym.info.typeParams, args) && !sym.lockOK)
throw new RecoverableCyclicReference(sym)

TypeRef(pre, sym, args)
if ((args eq Nil) && (pre eq NoPrefix))
sym.tpeHK // opt lean on TypeSymbol#tyconCache, rather than interning a type ref.
Copy link
Member

Choose a reason for hiding this comment

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

Could this be pushed further down into TypeRef.apply?

Copy link
Member Author

Choose a reason for hiding this comment

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

IIRC, we can't (easily) do that because tpeHK calls that TypeRef.apply, so we'd loop.

Copy link
Member

Choose a reason for hiding this comment

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

OK, I see.. We could clean up TypeRef construction one day.

@@ -1085,7 +1085,8 @@ trait Types
override def baseTypeSeq: BaseTypeSeq = supertype.baseTypeSeq
override def baseTypeSeqDepth: Depth = supertype.baseTypeSeqDepth
override def baseClasses: List[Symbol] = supertype.baseClasses
}
override def boundSyms: Set[Symbol] = emptySymbolSet
Copy link
Member

Choose a reason for hiding this comment

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

since mapOver recurses into components, it seems there are some unnecessary calls to resultType.boundSyms:

MethodType
    override def boundSyms = resultType.boundSyms ++ params

PolyType
    override def boundSyms = immutable.Set[Symbol](typeParams ++ resultType.boundSyms: _*)

and an unnecessary override in NullaryMethodType

    override def boundSyms = resultType.boundSyms

Copy link
Member Author
@retronym retronym Apr 20, 2017

Choose a reason for hiding this comment

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

Yep, I agree. Things seem to work with your proposed changes. I'm pushed a commit that actually avoids calling boundSyms altogether, which avoids the overhead of creating sets just to check if one of the from symbols exists in there, I'm pretty sure it will be faster to iterate over, e.g, MethodSymbol#typeParams directly and compare search for element to from.

We can actually short-circuit that search in many cases by pre-computing the range of symbol ids in from and by noting whether it has any term symbols.

Copy link
Member

Choose a reason for hiding this comment

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

Cool! boundSyms already confused me not long ago (I don't remember the details).

@@ -3788,7 +3788,7 @@ trait Types
private var uniques: util.WeakHashSet[Type] = _
private var uniqueRunId = NoRunId

protected def unique[T <: Type](tp: T): T = {
protected def unique[T <: Type](tp: T): T = {
Copy link
Member

Choose a reason for hiding this comment

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

< F438 /tr>
@@ -2928,6 +2938,34 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
// are a case accessor (you can also be a field.)
override def isCaseAccessorMethod = isCaseAccessor

def typeAsMemberOf(pre: Type): Type = {
Copy link
Member

Choose a reason for hiding this comment

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

Found 2 binary incompatibilities (22 were filtered out)
=======================================================
 * abstract synthetic method
   scala$reflect$runtime$SynchronizedSymbols$SynchronizedMethodSymbol$$$outer()scala.reflect.runtime.SynchronizedSymbols
   in interface
   scala.reflect.runtime.SynchronizedSymbols#SynchronizedMethodSymbol is
   present only in current version
 * abstract synthetic method
   scala$reflect$runtime$SynchronizedSymbols$SynchronizedMethodSymbol$$super$typeAsMemberOf(scala.reflect.internal.Types#Type)scala.reflect.internal.Types#Type
   in interface
   scala.reflect.runtime.SynchronizedSymbols#SynchronizedMethodSymbol is
   present only in current version

@@ -23,9 +23,6 @@ trait Namers extends MethodSynthesis {
import global._
Copy link
Member

Choose a reason for hiding this comment

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

i cannot find the commit referenced in the commit message (8dc4dbf0edde0f83b76c6a31d1f741689fa2e67b), i guess it's 1f6f7f8

if (mtpeRunId == currentRunId) {
if ((mtpePre eq pre) && (mtpeInfo eq info)) return {
mtpeResult
}
Copy link
Member

Choose a reason for hiding this comment

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

could merge the two ifs, also the return block is a bit funny

computeMemberType(sym)
}

def computeMemberType(sym: Symbol): Type = sym.tpeHK match { //@M don't prematurely instantiate higher-kinded types, they will be instantiated by transform, typedTypeApply, etc. when really necessary
Copy link
Member

Choose a reason for hiding this comment

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

the comment can probably be dropped, since adriaan removed it in 1f6f7f8

@retronym retronym added the WIP label Apr 20, 2017
createFromClonedSymbols(bs, restp)((ps1, tp1) => PolyType(ps1, renameBoundSyms(tp1)))
case ExistentialType(bs, restp) =>
case MethodType(ps, restp) if fromHasTermSymbol && fromContains(ps) =>
createFromClonedSymbols(ps, restp)((ps1, tp1) => copyMethodType(tp, ps1, tp1))
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why we don't need the recursive calls to renameBoundSyms anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

The result of this method is recursed into with mapOver, so we'll get back to here with the result type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to elaborate on this in a comment

Copy link
Member Author

Choose a reason for hiding this comment

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

The result of this method is recursed into with mapOver, so we'll get back to here with the result type.

Actually, taking another look at this, I think there is a potential problem with existential types, as newExistentialType collapses nested existential types. If some construction of existentials snuck past that, and an ExistentialType(qs1, ExistentialType(qs2, res)) appeared here, we'd fail to look at qs2.

  /** A creator for existential types which flattens nested existentials.
   */
  def newExistentialType(quantified: List[Symbol], underlying: Type): Type =
    if (quantified.isEmpty) underlying
    else underlying match {
      case ExistentialType(qs, restpe) => newExistentialType(quantified ::: qs, restpe)
      case _                           => ExistentialType(quantified, underlying)
    }

As per @adriaanm's offline suggestion, I'm going to split this change into a separate PR and address the review comments there.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, the old code was already assuming existentials has been flattened.

@retronym
Copy link
Member Author

My measurements suggest that the last two commits (removing boundSyms, and avoiding futile comparisons of eta-expanded type refs in <:< and =:=) offer a further 2% speedup. 🏎

@lrytz
Copy link
Member
lrytz commented Apr 21, 2017

Very nice!

@retronym retronym force-pushed the faster/symtab branch 7 times, most recently from 3af9df6 to b9c0dad Compare April 21, 2017 12:50
// a local class symbol. We can no longer assume that`mtpePre eq pre` is a sufficient condition
// to use the cached result here.
//
// Rather than throwing away the baby with the bathwater, lets at least try to keep the caching
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

case tp =>
// Correct caching is nearly impossible because `sym.tpeHK.asSeenFrom(pre, sym.owner)`
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this comment worth keeping around?

Copy link
Contributor

Choose a reason for hiding this comment

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

The first thing I'm going to type tomorrow is // is this comment worth keeping? I hope I don't accidentally read it six months from now, because it would blow my brain i.e. mind. The other one I should plant for my future self is // what were you thinking?

Copy link
Member Author

Choose a reason for hiding this comment

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

// Ceci n'est pas un commentaire

Copy link
Contributor

Choose a reason for hiding this comment

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

// comment?

val symHiInfo = symHi.info
if (symHiInfo == WildcardType) {
if (symHi.isTerm) (!symHi.isStable || symLo.isStable) // sub-member must remain stable
else true
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we guaranteed symHi.isAbstractType in this case? Can an alias type's info ever be a wildcard? (If so, the lower symbol's info must also be a wildcard)

Copy link
Member Author

Choose a reason for hiding this comment

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

The common case that I'm trying to optimize is when symHi is the member of a prototype for HasMember (e.g { def extensionMethod: ?}). I'll tweak this to restrict this new fast path to symHiInfo == WildcardType && symHi.isTerm` and let types flow down the the logic below.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've pushed this change now.

val r1 = tp1.typeSymbolDirect.isNonBottomSubClass(tp2.typeSymbolDirect) // OPT faster than comparing eta-expanded types
val r2 = isSub(tp1.normalize, tp2.normalize)
if (r1 != r2)
getClass
Copy link
Contributor

Choose a reason for hiding this comment

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

stray debug artifact?

createFromClonedSymbols(bs, restp)((ps1, tp1) => PolyType(ps1, renameBoundSyms(tp1)))
case ExistentialType(bs, restp) =>
case MethodType(ps, restp) if fromHasTermSymbol && fromContains(ps) =>
createFromClonedSymbols(ps, restp)((ps1, tp1) => copyMethodType(tp, ps1, tp1))
10000
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to elaborate on this in a comment

@@ -698,18 +703,30 @@ private[internal] trait TypeMaps {
// OPT this check was 2-3% of some profiles, demoted to -Xdev
if (isDeveloper) assert(sameLength(from, to), "Unsound substitution from "+ from +" to "+ to)

private[this] var fromHasTermSymbol = false
private[this] var fromMin = Int.MaxValue
Copy link
Contributor

Choose a reason for hiding this comment

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

Kind of like a mini-bloom filter?

Copy link
Member Author

Choose a reason for hiding this comment

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

Funny, I was re-reading about bloom filters while making this change. I guess this is kindred (we can cheaply determine non-existence in a set without iterating the full set), but OTOH we there isn't any superimposition of information.

I did learn about an Zatocoding in my travels, sort of an analog bloom filter for librarians.

retronym added 12 commits April 26, 2017 13:11
Like using AnyRefMap, we can optimize this datastructure with the knowledge
that is only holds AnyRef-s, and we can bypass BoxesRuntime for equality/hashing.
For a no-args, no-prefix type ref, we can bypass the call
to TypeRef.apply, and with it, the hash-consing of the type,
by asking the Symbol for its type constructor ref, which is
cached.
Avoids the megamorphic call to `Type#annotations` in favour
of an instanceof check.
We find our way to this method with wildcard types in the
search for implicit views. This commit short circuits the
computation in this case, avoiding an as-seen-from operation
on the low symbol's info
This method is call in symbol substitution, which is a hot
path during typechecking.

This commit observes that certain types of types don't define
any bound symbols, and immediately returns an empty set.
These types of types are:

    ThisType     this.type
    SuperType    A.super.B
    ConstantType 42
    SingleType   foo.type
    TypeBounds   >: L <: B

When a type reports an empty set of bound symbols, symbol substution
_does_ recurse into the compoments of the type (with `mapOver`), so
we still find bound symbols, for instances, an existential type in
one of the type bounds.

Before this commit, calling `ThisType#boundSyms` came to the same
result via the implementation inherited from `SimpleTypeProxy`, which
incurred an needless call to `ThisType#typeOfThis`.
If we arrive at this method with NoSymbol as the candidate symbol,
avoid an as-seen-from on the low side of the comparison.
Directly call hashCode on the arguments, rather than
indirecting through ##.
Avoid use of BoxesRuntime equals in favour of direct use
of Object#equals.
Unfortunately, we've had to add a lot more casts into the code
to workaround corner cases in the Fields transform. This commit
avoids an inefficiency in synthesizing these trees, by avoid
a call to as-seen-from.
@retronym retronym force-pushed the faster/symtab branch 2 times, most recently from babf510 to 6998cf1 Compare April 26, 2017 04:12
@retronym retronym removed the WIP label May 3, 2017
@retronym
Copy link
Member Author
retronym commented May 3, 2017

I believe I've addressed the review comments here.

Copy link
Contributor
@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

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

Awesome job, please update the description with the latest stats

Copy link
Contributor
@adriaanm adriaanm left a comment

Choose a reason for hiding this comment

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

The subtype change LGTM, but have a question on the sameType one.

def retry(lhs: Type, rhs: Type) = ((lhs ne tp1) || (rhs ne tp2)) && isSameType(lhs, rhs)
def retry(lhs: Type, rhs: Type) = ((lhs ne tp1) || (rhs ne tp2)) && {
def sameSyms(tp1: Type, tp2: Type) = tp1.typeSymbolDirect eq tp2.typeSymbolDirect
val skipRetry = lhs.isInstanceOf[PolyType] && rhs.isInstanceOf[PolyType] && sameSyms(tp1, lhs) && sameSyms(tp2, rhs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you convince me that we don't need to take into account potential differences in type params / type application due to normalization? I'm not sure how, but I'm also not sure it's impossible that normalizePlus could take e.g., [T, U](T, U) to [T, U](U, T) for one of rhs/lhs, so that now [T,U]Swap[U,T] =:= [T,U]Tuple2[T,U]

Also, this feels easier to follow:

      val skipRetry =
        (lhs eq tp1) && (rhs eq tp2) ||
          (lhs.isInstanceOf[PolyType] && rhs.isInstanceOf[PolyType] && sameSyms(tp1, lhs) && sameSyms(tp2, rhs))

      !skipRetry && isSameType(lhs, rhs)

Copy link
Member Author

Choose a reason for hiding this comment

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

I've reworked that commit a to be more in line with the subtyping change (computing skip before actually calling normalize). Given that it is pretty subtle, let's take our time to review it in #5885, rather than in this omnibus.

Restrict the fast path to term symbols (which is the important case
for performance) to allow the existing code to handle type symbols.
@retronym
Copy link
Member Author
retronym commented May 4, 2017

@paplorinc I've added the benchmark results to the description.

@l0rinc
Copy link
Contributor
l0rinc commented May 4, 2017

@retronym the cnt seems to be different for the two runs, is that expected? I don't know how to compare them this way.

@retronym
Copy link
Member Author
retronym commented May 4, 2017

The benchmark runs for a fixed duration, so after the speed up then count of executions is higher

The results suggest a 3.5% speed up. 1251/1299

@l0rinc
Copy link
Contributor
l0rinc commented May 4, 2017

Thanks, good job!

 Conflicts:
	src/reflect/scala/reflect/internal/Types.scala

Conflict was trivial related to commented out code.
@retronym
Copy link
Member Author

@adriaanm I've resolved the merge conflict here.

@retronym
Copy link
Member Author
retronym commented May 23, 2017

ping @adriaanm for a (final?) review round here; ditto for #5875 #5882.

@retronym retronym merged commit 56daf3a into scala:2.12.x May 24, 2017
@adriaanm adriaanm added the performance the need for speed. usually compiler performance, sometimes runtime performance. label May 25, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance the need for speed. usually compiler performance, sometimes runtime performance.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0