-
-
Notifications
You must be signed in to change notification settings - Fork 28
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
Discussion about lazy val definitions #53
Comments
Hm. I'm confused when you mention
But then, So if you change attr definitions from That is... a lot of short lived allocations that currently don't happen because those builders are cached by What I'm saying, I don't really see how you can inline |
Thanks for the response! I mentioned case class Attribute[T](key: String, value: T)
@inline class AttributeBuilder[T](key: String) {
@inline def :=(value: T): Attribute[T] = Attribute[T](key, value)
} Now if I write something like the following, I would actually allocate an lazy val builder = new AttributeBuilder[String]("contenteditable")
val result = builder := "true" The above compiles to: val builder = new AttributeBuilder[T]("contenteditable")
val result = Attribute[String](builder.key, "true") If I now instead define my builder as an inline def, I will never actually allocate an @inline def builder = new AttributeBuilder[String]("contenteditable")
val result = builder := "true" The above compiles to: val result = Attribute[String]("contenteditable", "true") Now, there will be constructions where you cannot avoid allocating a builder because inlining would not work. If for example I had a non-inlining function returning an So, what I would gain is hopefully less runtime overhead for constructing nodes in OutWatch, because intermediate builders can be inlined for the most part. Currently the I guess, we would want to test how reliable inlining is to be sure it makes sense. Furthermore, this will not work that well on the JVM, because scala-js does a lot more for inlining functions than just the scala compiler. |
Interesting, I didn't realize classes too could be inlined in ScalaJS. I'm guessing the behaviour of that is somewhat similar to value classes ( -- Assuming class inlining works sufficiently well, let's see what we can get out of it:
Overall, intuitively, this seems like a small and controversial net gain that's dependent on fragile and probably undocumented inlining behaviour, so I'm skeptical about this approach. If you have a build of dom-types with inline defs, have you tried running your project with it? How does it affect your un-gzipped |
Sorry for taking so long with my resonse!
As far as I know there are reasons why things cannot be inlined like returning an inline class in a method that itself is not inlined. But I agree, as of now, this feature is definitely under-documented. Though, inlining in scala-js works really well for me, in my experiments it was much more reliable than Another cool thing you can do with inlining is unrolling lambda functions. For example: @inline def doSomething(f: String => String): String = f("test")
val result = doSomething(_ + "!") This can be compiled to:
Inlining functions can be really nice for tight loops and building zero-cost abtractions.
We need to do benchmarks on this and compare the output of fullOpt as you proposed -- otherwise we just do not know. I imagine using Strings directly would be the more performant than the lazy val construction.
Why do you need equality on the builders? Relying on the builders to be cached for proper equality seems a bit fragile to me anyhow. I think having a proper equals method is the way to go. What if someone creates a builder without using the provided lazy vals? Or what if someone extends the trait with properties multiple times?
I have a branch and tried to reduce the generated code for creating virtual nodes in outwatch, but did not have time to investigate this in more detail, yet. I will keep you updated on this one - just not sure when I will find the time. |
I've been looking for more info of how inline classes work, here are a couple explanations: https://gitter.im/scala-js/scala-js?at=561531aece6e633c4518d5a4 So if I understand correctly, all usages of a given instance of inline class need to be contained within a single method in order for that instance to be inlined. So, a method can not return an inline class – that class would not be inlined then. Unless the method itself is inlined. Then the same criteria applies to the method that calls this method. So... perhaps if we inline enough methods and classes, we'll get the original class inlined as well. However, looking over Laminar code, I suspect there will be a tradeoff between reducing type safety and increasing bundle size. Essentially to deal with methods into which inlined classes escape we can either change their type signatures to receive individual fields of those inlined classes to avoid them escaping, or we can inline those methods to expand the inline boundary, but that would mean code duplication in the bundle. Hope that makes sense. You can see what I mean in DomBuilder's I'm curious to hear if your inline def branch produces the desired results whenever you have time to look into that. |
Nice, thank you for the links to the gitter conversation!
I agree that optimizing this properly is a thin line, because you really have to take care that everything in between is inlined. The intended optimization can break when you add new code or more abstractions. That is the sad part of it :/
Ah I see, you are building these setters as your Modifiers. My first intuition would be to not inline the |
The thing is, I do need to inline both the Alternatively, I would need to change Also I'm not sure if changing the signature like that would even work for inlining purposes, as But there's more. When we build But And remember, we need Setter inlined because otherwise the original thing we wanted to inline, ReactiveHtmlAttr, would escape method boundary and would not be inlined. The more I look into this, the more fragile and impossible this seems, unfortunately :| This basically needs a lot of painful testing to verify that inlining is actually happening, let alone what effect it has on bundle size and performance. |
Copied from #86: Scala.js 1.11 (news) now removes unused fields. If I understand correctly, this means that we can have e.g. val div = htmlTag("div") instead of it being a lazy val, and if you never use div-s in your app, div won't be included in the JS bundle, similar to how it works today with lazy vals. This would be great for us because having a hundred lazy vals for all the tags and properties that you use obviously comes with some overhead, and even if it's not really noticeable, it would be great to simplify. However, we need to test whether the Scala.js optimizer is smart enough to recognize our implementations of def htmlTag as "pure", and thus removable, otherwise we won't win anything. For Laminar the implementation instantiates a class HtmlTag which inherits from a trait with two public val-s, the constructor doesn't do anything else other than set those vals. For Outwatch and other consuming libraries, I'm not sure, but I suspect it's something similar. I'm hoping all our implementations are pure enough. Since Scala DOM Types 17.x, this decision is now on the authors of libraries like Laminar, Outwatch, and Calico – if anyone tries any non- |
Currently, domtypes defines dom attributes, properties, styles, events and elements as
lazy val
.Now, we use domtypes in OutWatch, where most of the things we are constructing are builders. For example, the attribute
contenteditable
is anAttributeBuilder
to which you can assignBoolean
values. So you would write something likecontenteditable := true
. This is evaluated intoAttribute("contenteditable", "true")
. Ideally, our code should be compiled into the evaluated Attribute directly. Now, ourAttributeBuilder
already usesinline
annotations to inline most of the construction part, but the compiler cannot inline the whole expression, becausecontenteditable
is defined as a lazy val.I would like to evaluate whether we can use
@inline def
instead oflazy val
for the definitions in domtypes. What do you think? What kind of tests/evaluations would you like to see?The text was updated successfully, but these errors were encountered: