What does the @elidable annotation do in Scala, an

2020-05-25 01:00发布

问题:

I've noticed in some of the scala library code, notably Predef, there is code like:

/** Tests an expression, throwing an `AssertionError` if false.
*  Calls to this method will not be generated if `-Xelide-below`
*  is at least `ASSERTION`.
*
*  @see elidable
*  @param p   the expression to test
*/
@elidable(ASSERTION)
def assert(assertion: Boolean) {
if (!assertion)
  throw new java.lang.AssertionError("assertion failed")
}

This annotation allows me, at compile time, to eliminate code. When I compile with -Xelide-below MAXIMUM, does it

  1. remove the method and all calls to it? (If so, what happens if another library expects this method to be there?), do we get a NoSuchMethodError or whatever?
  2. leave the method there, but remove all of the code from the method, leaving an empty method?
  3. just remove the calls to the method, but leave the method there?

Can I use it to reduce the compiled size of the class? So if I had:

class Foobar {
    // extremely expensive toString method for debugging purposes
    @elidable(FINE) def toString(): String = "xxx"
}

and compiled with -Xelide-below WARNING would the toString in this class disappear altogether? Note that in this example, I would want the method to be removed from the class, because I wouldn't want the possibility of it being called.

Second part: I've seen it suggested that this be used for eliminating debugging logging code. Given that most frameworks (log4j notably) allow runtime setting of logging level, I don't think that this is a good use case. Personally, I would want this code to be kept around. So apart from the assert() methods in Predef, what is a good use case for @elidable?

回答1:

Short answer

Both method and all calls to it simply disappear. This might be a good idea to use for logging since every logging framework introduces some overhead when logging is called but a given level is disabled (computing the effective level and preparing arguments).

Note that modern logging frameworks try to reduce this footprint as much as possible (e.g. Logback optimizes is*Enabled() calls and SLF4S passes message by name to avoid unnecessary string concatenations).

Long one

My test code:

import scala.annotation.elidable
import scala.annotation.elidable._

class Foobar {
    info()
    warning()

    @elidable(INFO) def info() {println("INFO")}
    @elidable(WARNING) def warning() {println("WARNING")}
}

Proves that with -Xelide-below 800 both statements are printed while with 900 only "WARNING" appears. So what happens under the hood?

$ scalac -Xelide-below 800 Foobar.scala && javap -c Foobar

public class Foobar extends java.lang.Object implements scala.ScalaObject{
public void info();
//...

public void warning();
//...

public Foobar();
  Code:
   0:   aload_0
   1:   invokespecial   #26; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   invokevirtual   #30; //Method info:()V
   8:   aload_0
   9:   invokevirtual   #32; //Method warning:()V
   12:  return
}

As you can see this compiles normally. However when this instruction is used:

$ scalac -Xelide-below 900 Foobar.scala && javap -c Foobar

calls to info() and the method itself disappears from the bytecode:

public class Foobar extends java.lang.Object implements scala.ScalaObject{
public void warning();
//...

public Foobar();
  Code:
   0:   aload_0
   1:   invokespecial   #23; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   invokevirtual   #27; //Method warning:()V
   8:   return

}

I would expect that NoSuchMethodError is thrown at runtime when removed method is called from client code compiled against Foobar version with lower elide-below threshold . Also it smells like good old C preprocessor, and as such I would think twice before employing @elidable.



回答2:

As a complement to Tomasz Nurkiewicz's answer two comments.

(1) C++ style

Because I came from C++ I've defined

/** ''Switch'' between '''Debug''' and '''Release''' version. */
object BuildLevel {
  type only = annotation.elidable
  final val DEBUG = annotation.elidable.INFO
}

and use this in good old C++ preprocessor style like

import BuildLevel._
@only(DEBUG)
private def checkExpensive(...) {
  ...
}

override def compare(that: ): Int = {
  checkExpensive(...)
  ...
}

to mark expensive checks (check of pre-conditions or invariants that must always holds true) that I want to switch off in release builds.

Of course that's just similar to the assert use case except for the difference of refactoring out expensive code in a separate method that should be switched off as a whole. But all this is only worthwhile for really expensive checks. In a 10k lines project I have only 3 marked checks. Cheaper tests I wouldn't switch off and leave in the code, because they increase its robustness.

(2) Unit signature

This approach is suitable only for methods with a (...) => Unit signature. If one use a result of such a switched off method like

@only(DEBUG)
def checkExpensive(that: Any): Int = {
  4
}
val n = checkExpensive(this)

at least my Scala 2.9.1.final compiler crashes. However, there is not much sense in such a signature. Because: Which value should such a switched off method return?



回答3:

Actually, expressions can't just disappear, because they have a result. When you elide an invocation of a method of result type Boolean, you wind up with false, and so on.

There was an issue a few months after this question was posted to settle what eliding Nothing does. The outcome was to elide to ???.



标签: scala