Skip to content
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

Use Scala 3 threadUnsafe lazy vals #1818

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

igor-vovk
Copy link
Contributor

@igor-vovk igor-vovk commented Feb 15, 2025

Hi!

Scala 3 introduced faster mechanism of initialization for lazy vals: link to docs.

I want to propose adding it to some of the lazy val's in the generated outputs.

The reasoning is that there would be no harm if in the worst case scenario default values are initialized more than once and then GCed, but instead there will be a) no thread-locks during the initialization b) much less byte-code and faster compilation, since they are used a lot. Lazy vals for variables inside of the *Proto classes (decoding of the protobufs) left as is (no @threadUnsafe annotation added), since it may be more costly to initialize those values more than once.

Here is the example of 2 classes, with annotation and without:

class LazyVal {
  lazy val x = "test"
}

class ThreadUnsafeLazyVal {
  @scala.annotation.threadUnsafe
  lazy val x = "test"
}

And here is the bytecode that is produced (decompiled to Java code):

/// LazyVal.decompiled.java

import scala.runtime.LazyVals;
import scala.runtime.LazyVals.;

public class LazyVal {
    public static final long OFFSET$0;
    private volatile Object x$lzy1;

    public LazyVal() {
    }

    static {
        OFFSET$0 = .MODULE$.getOffsetStatic(LazyVal.class.getDeclaredField("x$lzy1"));
    }

    public String x() {
        Object var1 = this.x$lzy1;
        if (var1 instanceof String) {
            return (String)var1;
        } else {
            return var1 == scala.runtime.LazyVals.NullValue..MODULE$ ? null : (String)this.x$lzyINIT1();
        }
    }

    private Object x$lzyINIT1() {
        while(true) {
            Object var1 = this.x$lzy1;
            if (var1 == null) {
                if (.MODULE$.objCAS(this, OFFSET$0, (Object)null, scala.runtime.LazyVals.Evaluating..MODULE$)) {
                    Object var2 = null;
                    Object var3 = null;

                    try {
                        var8 = "test";
                        if (var8 == null) {
                            var2 = scala.runtime.LazyVals.NullValue..MODULE$;
                        } else {
                            var2 = var8;
                        }
                    } finally {
                        if (!.MODULE$.objCAS(this, OFFSET$0, scala.runtime.LazyVals.Evaluating..MODULE$, var2)) {
                            LazyVals.Waiting var5 = (LazyVals.Waiting)this.x$lzy1;
                            .MODULE$.objCAS(this, OFFSET$0, var5, var2);
                            var5.countDown();
                        }

                    }

                    return var8;
                }
            } else {
                if (var1 instanceof LazyVals.LazyValControlState) {
                    if (var1 == scala.runtime.LazyVals.Evaluating..MODULE$) {
                        .MODULE$.objCAS(this, OFFSET$0, var1, new LazyVals.Waiting());
                        continue;
                    }

                    if (var1 instanceof LazyVals.Waiting) {
                        ((LazyVals.Waiting)var1).await();
                        continue;
                    }

                    return null;
                }

                return var1;
            }
        }
    }
}

/// ThreadUnsafeLazyVal.decompiled.java

public class ThreadUnsafeLazyVal {
    private String x$lzy2;
    private boolean xbitmap$1;

    public ThreadUnsafeLazyVal() {
    }

    public String x() {
        if (!this.xbitmap$1) {
            this.x$lzy2 = "test";
            this.xbitmap$1 = true;
        }

        return this.x$lzy2;
    }
}

What do you think about this change?

@igor-vovk
Copy link
Contributor Author

Seems that sources emitted for Scala 3 are still tested against Scala 2.13. This change seems to break this test. Out of curiosity, could you please share, what is the idea behind it? That latest Scala 2.13 should work with Scala 3 sources?

@thesamet
Copy link
Contributor

It's been a while, but I found this statement in https://scalapb.github.io/docs/customizations/:

By default, ScalaPB generates Scala sources that are compatible with both Scala 2 and Scala 3. To generate sources that can be compiled error-free with -source feature on Scala 3 or with -Xsource:3 on Scala 2.13, set scala3_sources to true or pass the scala3_sources generator parameter.

See also #1316 - some users were (are) compiling their project with this flag.

@igor-vovk
Copy link
Contributor Author

I've been thinking about it for some time, and I had interesting observations:

I couldn't find any reasons why someone wants to generate Scala 2 sources when compilation target is set to Scala 3.
I do understand what is the purpose of enabling Scala 3 sources when compiling the library for Scala 2, this makes sense for me: to cross-compile or reuse the same sources for both versions of Scala.

That's why the question pops up: should Scala 3 source generation to be forced when compiling for Scala 3? So the option scala3_sources becomes obsolete for Scala 3 and starts making sense only for Scala 2.13.

This could then unblock optimizations like this, that will be enabled only for Scala 3. Also with time the scala3_sources option can become deprecated when (let's say that hopefully it will happen one day) Scala 2 support will be dropped.

If you see sense in making scala3_sources effective only when compiling for Scala 2, but to be always enabled for 3, I would then be happy to research how to check Scala target version during compilation and will prepare a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants