Scala Heaven and Hell — Michael Slinn

I think you are continuing to make this issue more complicated than it is.

If you declare an opaque type alias to a basic type such as Int or String, there cannot possibly be a performance cost beyond just using one of those types. The performance gain compared to a wrapper can be zero or positive, but it CANNOT BE NEGATIVE. And that is guaranteed, so YOU DON’T HAVE TO BOTHER CHECKING!

You keep implying that the motivation for opaque types is performance, but that is misleading. The motivation is enhanced basic type checking with minimal boilerplate and without TAKING A CHANCE of sacrificing performance.

2 Likes

But of course there might be a performance cost, and even the SIP admits this:

The extension methods synthesized for implicit value classes are not static. We have not measured if the current encoding has an impact in performance, but if so, we can consider changing the encoding of extension methods either in this proposal or in a future one.

The motivation – according to the SIP – is to be able to differentiate between different type aliases of the same type – say Id and Password (both String) – so that the compiler would not allow to mistakenly mix between the values.

The SIP admits that one solution for such a scenario is via wrappers, but that there is a performance penalty with them, that “in many cases [this] is fine but in some [it] is not”; hence, they may not be a sufficient solution.

The SIP then suggests a new solution for this scenario, and it needs to provide a proof – via both theory and evidence – that this solution does not suffer the same performance penalty; otherwise, it’s no different than the wrappers solution, and therefore redundant.

@eyalroth - Argh. Some things are obvious to people with familiarity with an area and don’t require additional evidence.

Do you really think everyone who is saying that yes it is a big deal and have personally struggled with it are making a mistake?

Here’s an example of a 13% slowdown (and ~4x memory usage IIRC) due to boxing into an array (preboxed, before the benchmark, which is a near ideal case):

case class A(i: Int) extends AnyVal { def add(j: Int) = i + j }
val a = Array.range(0, 1000)
val aa = a.map(ai => A(ai))

th.pbenchOff(){
  var i, s = 0
  while (i < a.length) { s = A(a(i)).add(s); i += 1 }
  s
}{
  var i, s = 0
  while (i < aa.length) { s = aa(i).add(s); i += 1 }
  s
}

Output:

Benchmark comparison (in 690.1 ms)
Significantly different (p ~= 0)
  Time ratio:    1.12966   95% CI 1.11298 - 1.14635   (n=20)
    First     584.2 ns   95% CI 580.0 ns - 588.5 ns
    Second    660.0 ns   95% CI 651.5 ns - 668.5 ns
res2: Int = 499500

Let’s fold something. Starting with:

case class A(i: Int) extends AnyVal { def +(that: A) = A(i + that.i) }

def fold(xs: Array[Int], op: (Int, Int) => Int) = {
  var i, acc = 0
  while (i < xs.length) {
    acc = op(acc, xs(i))
    i += 1
  }
  acc
}

def fold2(xs: Array[A], op: (A, A) => A) = {
  var i = 0
  var acc = A(0)
  while (i < xs.length) {
    acc = op(acc, xs(i))
    i += 1
  }
  acc
}

We run:

val a = Array.range(0, 1000)
val aa = a.map(ai => A(ai))

val th = new ichi.bench.Thyme
th.pbenchOff(){ fold(a,  _ + _) }{ fold2(aa, _ + _).i }

Now instead of 13% longer it takes 110% longer:

Significantly different (p ~= 0)
  Time ratio:    2.10579   95% CI 2.08184 - 2.12975   (n=20)
    First     352.6 us   95% CI 349.7 us - 355.5 us
    Second    742.4 us   95% CI 736.6 us - 748.3 us

And we can just keep going along these lines, adding abstraction and accumulating increasingly bad penalties without any warning.

So, bottom line is that the reason we “don’t need evidence” is that we already have a ton of it, and creating more is time-wasting pedantry.

Hopefully you’ve learned something from these microbenchmarks so it’s not a complete waste of time.

Edit: in case it isn’t obvious, the opaque type versions either wouldn’t compile, or would produce code identical to the primitive versions (by design). Either way is fine, really–it’s thinking you’re okay but it not being okay which really gets you.

2 Likes

I don’t think eyalroth is disputing that the old way (wrappers) can degrade performance. I think he is saying that the new way (opaque types) can also possibly degrade performance. I didn’t think that was possible, but I don’t know enough about it to be 100% certain. Can a scala compiler expert confirm that opaque types can’t possibly degrade performance compared to just using basic types?

1 Like

I’ve never implied that there is a mistake. What I said is that the performance gain may very well be quite insignificant for most scenarios (and that cannot be demonstrated with micro benchmarks), so merely claiming that “opaques are better” is misleading without giving any real world context, examples and benchmarks.

FYI you can somewhat emulate opaques in Scala 2 (credit):

object Logarithm {
  type Logarithm
  def apply(d: Double): Logarithm = math.log(d).asInstanceOf[Logarithm]
  private[this] def unwrap(log: Logarithm): Double = log.asInstanceOf[Double]

  implicit class Ops(x: Logarithm) {
    def toDouble: Double = math.exp(unwrap(x))
    def + (y: Logarithm): Logarithm = Logarithm(x.toDouble + y.toDouble)
    def * (y: Logarithm): Logarithm = x + y
  }
}

You mean, despite people-whose-job-it-is-to-know basically all agreeing that this is the way to do it, you want a wide variety people who have an extensive code base to make wide-ranging changes that they expect will trash their performance, and carefully document the changes and the impact; and they should do all this in order to save you the trouble from having to learn about how to tune Scala code for performance?

No thank you. You’re asking for way too much. Sometimes it’s appropriate to defer to experts. It’s never appropriate for the path to expertise to be obscured, but sometimes things are too burdensome to make obvious to non-experts.

Also, you missed the extends AnyVal on the implicit class Ops. This can be critical for Scala 2 performance (the JVM doesn’t always notice it can elide the wrapper class). And yes, I know the trick already.

This is absolutely not what you can conclude from the situation you described. That something isn’t heavily documented doesn’t mean that it’s misleading. Unproven is different from wrong. Non-obvious is also different from wrong.

2 Likes

I’m not a compiler expert, but I’m assuming it works like this: if B is an opaque alias of A, the compiler will first type check assuming A and B are different types, and then replace B by A.

For example:


opaque type MyFloat = Double

def calculate(x: MyFloat, y: MyFloat): MyFloat = ???

will check that arguments and return value are MyFloat, and after checking that will rewrite it to:


def calculate(x: Double, y: Double): Double = ???

By design, the performance is exactly as if Double had been used directly.

1 Like

Context worth noting, in support of @Ichoran’s point: this proposal originally came from Erik Osheim, one of the leads of the Spire project, who is one of the experts on Scala performance. This isn’t coming from some random nobody – it was designed by folks who know the system extremely well, specifically to support performance-critical, strongly-typed libraries…

1 Like

I have the notion that you’ve completely missed my point.

I was pointing out how the speaker of the original talk in this thread explains that it’s important for us the users to demand benchmarks for Scala features (6:44); yet he also later describes how opaques are better in performance and explains the reasoning behind this (1:00:37), but doesn’t point to any benchmarks that have been done to prove this, despite explaining earlier how important those are.

This discussion is not about questioning the performance of opaques; it is about calling for more transparency from Scala’s lead developers. Sure, you may deem this a waste of time, but that is exactly what the speaker is advocating for.

Partly, yeah, I missed your point. So did you, though.

Without watching it (I don’t have time) I can’t tell whether this is an inconsistency in the speaker’s point of view. If he means, “It’s important to know how fast things are–please just benchmark them once and tell us, instead of making each of us do it over again”, yeah, that’s a reasonable desire. If he means, “it’s critical to publish benchmarks that demonstrate incontrovertibly what everyone already knows”, no, that’s rubbish, whether or not he contradicts himself later on by lauding something that isn’t benchmarked this way.

If you’re merely critiquing the speaker, I don’t have an opinion to offer as to whether there is hypocrisy here. I’d have to watch the video. Except I don’t really care about the video. I do care about opaque types.

This is ridiculous.

First, you are mischaracterizing yourself because you are actually questioning the performance of opaques. For example:

You wrote all that stuff. This isn’t what someone says when they’re “not questioning the performance of opaques”. Of course you are. Repeatedly.

Secondly, there’s absolutely no issue with the transparency here. You admit that you aren’t familiar with writing high-performance Scala code. e.g.:

The only lack of transparency here is not always being willing to hold the hand of someone who isn’t willing to do their homework.

Don’t get me wrong–I think hand-holding is fantastic when you can spare the time for it. Education is very important. But, look, you’re right; not everyone writes code that has bottlenecks for which reducing boxing is important. So the educational mission is not that important. The feature is, especially for people who write the fast code that everyone else gets to call and benefit from.

The decisions about opaque types were made openly, for reasons and in discussions that are public. The supposed benefits can be checked by tools that are freely available with plenty of documentation for how to get the stuff working. Is it easy and simple to do so? No. Unfortunately, performance testing on the JVM is difficult, especially for low-level features like this which tend to sneak into cracks which profilers can’t even see.

But it’s absolutely transparent.

So, in summary: opaques by design improve performance by making it structurally impossible to fall into a particular trap that AnyVal was supposed to avoid but often didn’t. People who are experts in writing high-performance code benefit from exactly the kind of guarantee that opaques deliver.

1 Like

I indeed digressed, but that digression actually led to events that demonstrate the criticism that the speaker was talking about.

For what it’s worth, I don’t think the speaker is hypocrite, but rather that he acknowledges that he is a part of the problem. You could watch just the specific parts I’ve linked, there’s no need to listen to the entire talk.

He encourages the community to call out on experts (like himself) when they claim that a certain feature is better performing, but without providing enough reasoning and measurements, which then leads the community to blindly accept those as general practices without understanding the entire context and subtleties.

I used opaques as an anecdote to emphasize the speaker’s point. I am neither questioning their performance, nor the reasoning behind them, nor the transparency regarding them; after all, they are still WIP.

These are not examples of me questioning their performance. These are examples of me (a) asking to see results (transparency, not necessarily doubt), and (b) trying to emphasize that micro-performance gains may very well be irrelevant to most developers, as they are often hindered by other performance issues; i.e, you can choose to opt for a faster car, but that wouldn’t do you any good if traffic is constantly jammed.

I only admitted to have not been familiar and use AnyVal. There are so many other scenarios where high performing code is required – DB querying, heavy regex usage, XML processing, tag search indexing, networking (be it HTTP, SSH or what not), heavy disk usage, etc. – that do not require a boxing mitigation solution in any way.

Encouraging developers to blindly use every micro-optimization technique out there whenever they write “high performing code”, without considering whether that optimization makes any difference, is the root of all evil. Those micro-optimizations have other costs, such as complexity and readability, and should not be used blindly.

I am expecting that when opaques are published, they will be documented accordingly; that is, some micro-benchmarks should be published with them – similarly to collections’ characteristics – along with a clear explanation on their intended use: They are an advanced micro-optimization feature that should be used sparingly, only to solve specific performance issues after other means have been ruled out, and that merely using them does not guarantee any significant performance gain overall, and that overall users are encouraged to benchmark their application.

I don’t think this is too much to ask – after all, this is pretty minimal wording and simple benchmarks that I’m asking for. These may seem to you too trivial to even bother including, but you are an expert, and the audience of this documentation (Scala’s documentation) are everyday non-expert developers.

That’s not their intended use. Their intended use is as an opaque type alias, to reduce complexity, and increase readability. No feature should be used blindly, but whether they should be used sparingly or not is a stylistic choice. That they have better performance than AnyVal wrappers means that they can be used in performance-critical code, not that they shouldn’t be used outside of performance-critical code.

1 Like

Given you are already using AnyVals, then yes, they are definitely less complex and more readable (I still have some opinions on their currently proposed syntax, but that does not change this motivation).

The question is then why even bother using AnyVal or an opaque in the first place. It may end up resulting in a style-centric choice between opaques and case class wrappers, but overall it would incur some added complexity to the language (and any code base where opaques are being used), as this is an additional feature that a developer needs to be familiar with.

To reduce complexity and increase readibility.

1 Like

I’ve never seen a case where AnyVal is used because it is a more readable and less complex solution, and I believe that the motivation behind the feature was always about performance:

Properly-defined user value classes provide a way to improve performance on user-defined types by…

AnyVal

Value classes are a new mechanism in Scala to avoid allocating runtime objects. This is accomplished through the definition of new AnyVal subclasses.

Value Classes and Universal Traits | Scala Documentation

Sorry, I meant for opaque types, not for AnyVal.

1 Like

Sure, that’s an educational mission. That’s valuable. It’s not always practical, because these things take work, but it’s great when we can have it.

I’ve invested time to do this personally, too: at Scala Days 2013 (I think?) I gave an entire talk on reasoning about and measurements of the performance of various ways of handling error conditions. But even for that talk, getting real-world usage differences was overly burdensome and I didn’t really do it.

Fair enough, but the people writing the regex engines, XML processors, search strategies, and network stack all have to worry about boxing. (DB and file access have so much overhead that boxes are irrelevant even in the internals.)

You rely all the time upon people caring about this. For instance, earlier versions of Scalaz used ropes of boxed characters for string processing, which ended up being atrociously slow (like 100x slower) in common use cases.

I…ah…what?! I have no idea what you’re trying to claim any more. It looks like you’re contradicting yourself from post to post and even within a post. Maybe there’s some subtlety in what you’re claiming that isn’t coming through. Maybe you have an odd definition of “questioning” (e.g. “flatly rejecting any possibility of” as opposed to “I am not sure”).

Um…which is it? “Simple benchmarks” or “multiple scenarios each of which are more complex than microbenchmarks”?

I did post (micro)benchmarks, and you didn’t think much of them.

Nobody is doing this, though. The question is about providing a feature that allows a zero-cost abstraction to increase readability and correctness.

Right now, if you want improved correctness (which may or may not improve readability also) you need to have a wrapper class. But there’s a usually-unavoidable penalty when you use a wrapper class, and it’s kind of clunky. Opaque types are simultaneously penalty-free and less clunky! Those are both upsides compared to the current state of affairs. The downside is that there’s another thing to know about.

(Aside–that quote is badly dated. As far as performance-related woes go, failing to design and/or optimize when needed is the source of at least equal evil.)

I…don’t even…

Are you noticing what I’m writing?


Opaque types allow people to ask the compiler’s type system to distinguish, at compile time, sets of instances of the same underlying type. One can do this in a relatively low-boilerplate way, and because it has very low to zero cost, one basically never needs to worry about avoiding this due to performance concerns, unlike wrapping with classes. So you can use the feature whenever it makes sense for correctness and clarity.

The existing feature, AnyVal, was supposed to but does not do this in practice, and should be retired.

1 Like

Sure, no one is diminishing the importance of micro-optimizations in the grand scheme of things, but that doesn’t necessarily mean that the everyday (Scala) developer should be familiar with those. More importantly, they should not be misguided to use them for optimizations when such optimizations are not required, which is often their case.

Perhaps I was too subtle. Let me try again:

  1. I agree to a certain extent with the speaker in that there should be more transparency regarding Scala’s documentation in general – more examples, more measurements (depends on the feature).
  2. I am not questioning opaques’ transparency as they are still WIP.
  3. I am asking that at the end, when opaques are no longer WIP, there will be some form of measurements available for the sake of transparency.

I would obviously prefer multiple benchmarks of complex real-world scenarios, but given that this might be hard to come by, I would compromise for “simple” (easy to grasp) micro-benchmarks – like the ones you provided here – along with an explanation of the mechanism, and preferably at least one anecdotal real-world example of where this technique has been proven to be valuable via some benchmarking (otherwise how can you tell that the technique was valuable?) which are cumbersome / time consuming to replicate.

I don’t know exactly what you mean by “clunky”, but I personally prefer a wrapper case class to (the currently proposed) opaque types from any perspective other than performance. But this is obviously a matter of opinion.

If that motivation was the only motivation for the proposal, then I would’ve imagined a much simpler and more concise solution. I actually suggested such a solution in the contributors thread, but perhaps such a solution is not possible.

It seems to me that the proposal evolved to not only serve that motivation, but to provide a better performing implementation of value classes, which serve as better performing wrappers. There are many other reasons why one might choose to use wrappers; they are not used just for distinguishing instances of the same underlying type.

Why do you think that would be the case here more than elsewhere?

I don’t understand why “transparency” is the right word to use here. What are you trying to convey by using “transparency” instead of, say, “comprehensiveness” (obviously in a different but related sentence)?

I don’t understand why “transparency” is the right word here, either, as opposed to, say, “tutorial” (again, different sentence but with similar sentiment).

This is nice to aspire to, but I don’t understand why you think this needs so much attention. Or do you think everything should get this much attention, and you’re just using this as an example?

Inasmuch as the effort can be redirected at will, I’d much rather have a good deep explanation about how to use givens, how inline works, etc…

Which I would prefer depends on what I meant. If I have case class Meter(m: Double) {}, then a bunch of things are true about it, like that it’s a subclass of AnyRef, which I don’t intend to be true.

I agree that opaque types as newtypes could be better sugared in order to make them less awkward. But the current example has the virtue of simplicity: all that they do is control whether you can know what the underlying type is, and otherwise you just use standard language features.

Your suggestion doesn’t work because it assumes that all you want is for the new type to be propagated everywhere. But meters times meters are not meters, nor is Cents(i).leadingZeros a currency.

1 Like

Yes, I was using opaques as an example, not necessarily the case that needs the most attention. I completely agree with you that there are far more important features that demand better documentation.

Perhaps “transparency” is not the exact word to describe what the speaker is asking for, but I think you got the gist of what is desired – better documentation of purpose, rationale, theory, use cases, measurements, etc.

I believe I have an answer for that, but I think this belongs to another thread. Would you care to comment on the dedicated opaques thread? I’d love to continue discussing this – it’s interesting :slight_smile: