Can we avoid the allocation when using Option with Extractor

import java.util.concurrent.atomic.AtomicInteger

object Nat:
  inline def unapply(x: AtomicInteger): Option[Int] =  x.get() match {
    case n if n > 0 => Some(n)
    case _ => None
  }

object Main:
  def test(n: AtomicInteger): String =
    n match
      case Nat(m) => s"$m is a natural number"
      case _ => s"$n is not a natural number"

  def main(args: Array[String]): Unit =
    println(test(new AtomicInteger(5))) // Output: 5 is a natural number
// class version 52.0 (52)
// access flags 0x31
public final class Main$ implements java/io/Serializable {

  // compiled from: main.scala

  ATTRIBUTE Scala : unknown

  // access flags 0x19
  public final static LMain$; MODULE$

  // access flags 0x2
  private <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LMain$; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static <clinit>()V
   L0
    LINENUMBER 10 L0
    NEW Main$
    DUP
    INVOKESPECIAL Main$.<init> ()V
    PUTSTATIC Main$.MODULE$ : LMain$;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

  // access flags 0x2
  private writeReplace()Ljava/lang/Object;
   L0
    LINENUMBER 9 L0
    NEW scala/runtime/ModuleSerializationProxy
    DUP
    LDC LMain$;.class
    INVOKESPECIAL scala/runtime/ModuleSerializationProxy.<init> (Ljava/lang/Class;)V
    ARETURN
   L1
    LOCALVARIABLE this LMain$; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x1
  public test(Ljava/util/concurrent/atomic/AtomicInteger;)Ljava/lang/String;
    // parameter final  n
   L0
    LINENUMBER 11 L0
    ALOAD 1
    ASTORE 2
   L1
    LINENUMBER 12 L1
    ALOAD 2
    IFNULL L2
   L3
    LINENUMBER 12 L3
    ALOAD 2
    INVOKEVIRTUAL java/util/concurrent/atomic/AtomicInteger.get ()I
    ISTORE 4
   L4
    LINENUMBER 5 L4
    ILOAD 4
    ISTORE 5
   L5
    ILOAD 5
    ICONST_0
    IF_ICMPLE L6
    GETSTATIC scala/Some$.MODULE$ : Lscala/Some$;
    ILOAD 5
    INVOKESTATIC scala/runtime/BoxesRunTime.boxToInteger (I)Ljava/lang/Integer;
    INVOKEVIRTUAL scala/Some$.apply (Ljava/lang/Object;)Lscala/Some;
    GOTO L7
   L6
    LINENUMBER 6 L6
    GETSTATIC scala/None$.MODULE$ : Lscala/None$;
    GOTO L7
   L7
    ASTORE 3
   L8
    LINENUMBER 12 L8
    ALOAD 3
    INVOKEVIRTUAL scala/Option.isEmpty ()Z
    IFNE L2
    ALOAD 3
    INVOKEVIRTUAL scala/Option.get ()Ljava/lang/Object;
    INVOKESTATIC scala/runtime/BoxesRunTime.unboxToInt (Ljava/lang/Object;)I
    ISTORE 6
    ILOAD 6
    ISTORE 7
   L9
    NEW java/lang/StringBuilder
    DUP
    LDC 20
    INVOKESPECIAL java/lang/StringBuilder.<init> (I)V
    ILOAD 7
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    LDC " is a natural number"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L2
    LINENUMBER 13 L2
    NEW java/lang/StringBuilder
    DUP
    LDC 24
    INVOKESPECIAL java/lang/StringBuilder.<init> (I)V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
    LDC " is not a natural number"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L10
    LOCALVARIABLE n I L5 L6 5
    LOCALVARIABLE m I L9 L2 7
    LOCALVARIABLE this LMain$; L0 L10 0
    LOCALVARIABLE n Ljava/util/concurrent/atomic/AtomicInteger; L0 L10 1
    MAXSTACK = 3
    MAXLOCALS = 8

  // access flags 0x1
  // signature ([Ljava/lang/String;)V
  // declaration: void main(java.lang.String[])
  public main([Ljava/lang/String;)V
    // parameter final  args
   L0
    LINENUMBER 16 L0
    GETSTATIC scala/Predef$.MODULE$ : Lscala/Predef$;
    ALOAD 0
    NEW java/util/concurrent/atomic/AtomicInteger
    DUP
    ICONST_5
    INVOKESPECIAL java/util/concurrent/atomic/AtomicInteger.<init> (I)V
    INVOKEVIRTUAL Main$.test (Ljava/util/concurrent/atomic/AtomicInteger;)Ljava/lang/String;
    INVOKEVIRTUAL scala/Predef$.println (Ljava/lang/Object;)V
    RETURN
   L1
    LOCALVARIABLE this LMain$; L0 L1 0
    LOCALVARIABLE args [Ljava/lang/String; L0 L1 1
    MAXSTACK = 5
    MAXLOCALS = 2
}

I was thinking , the unapply is inline method, and the compiler will treat Some(n) and None special and rewrite the code to something like :

import java.util.concurrent.atomic.AtomicInteger

object Main:

  def main(args: Array[String]): Unit =
    println({
      (new AtomicInteger(5)).get() match
        case n if n > 0 => s"$n is a natural number"
        case x => s"$x is not a natural number"

    }) // Output: 5 is a natural number

1 Like

IIRC, if this is in the hot path, JVM will elide away the Option allocation, because the Option instance does not escape the method test

1 Like

you can eliminate the allocation with by-name extractors: Option-less pattern matching

  1. Do we need the AnyVal or is there no new Nat.
  2. Will the ref.get() be called twice if both the isEmpty and get both need to access the ref’s value.
1 Like