Scala:按名称呼叫,按值性能呼叫

时间:2017-10-19 08:04:24

标签: performance scala function

请考虑以下代码:

usuario.getText().toString() == usuarioAchequear

比较class A(val name: String) 的两个包装器:

A

class B(a: A){
  def name: String = a.name
}

class B1(a: A){ val name: String = a.name } B具有相同的功能。它们之间的内存效率和计算效率如何比较? Scala编译器会将它们视为同一个东西吗?

1 个答案:

答案 0 :(得分:3)

首先,我会说微观优化问题通常很难回答,因为他们缺乏背景。此外,此问题与按名称调用或按值调用无关,因为您的示例中没有按名称调用。

现在,让我们用scalac -Xprint:typer编译你的代码,看看会发出什么:

class B extends scala.AnyRef {                              
  <paramaccessor> private[this] val a: A = _;               
  def <init>(a: A): B = {                                   
    B.super.<init>();                                       
    ()                                                      
  };                                                        
  def name: String = B.this.a.name                          
};                                                          

class B1 extends scala.AnyRef {                             
  <paramaccessor> private[this] val a: A = _;               
  def <init>(a: A): B1 = {                                  
    B1.super.<init>();                                      
    ()                                                      
  };                                                        
  private[this] val name: String = B1.this.a.name;          
  <stable> <accessor> def name: String = B1.this.name       
};

在课程B中,我们保留对a的引用,并使用方法name调用name上的A值。

在课程B1中,我们在本地存储name,因为它直接是B1的值,而不是方法。根据定义,val声明中有一个为它们生成的方法,这就是它们的访问方式。

这归结为B1name分配的A字符串的附加引用这一事实。从性能角度来看,这有何重要意义?我不知道。在您发布的一般性问题下,我看起来可以忽略不计,但我不会对此感到困扰,除非您已经分析了应用程序并发现这是一个瓶颈。

让我们更进一步,并在此基础上运行一个简单的JMH微基准:

[info] Benchmark                        Mode  Cnt    Score     Error   Units
[info] MicroBenchClasses.testB1Access  thrpt   50    296.291 ± 20.787    ops/us
[info] MicroBenchClasses.testBAccess   thrpt   50    303.866 ± 5.435    ops/us
[info] MicroBenchClasses.testB1Access   avgt    9    0.004 ±   0.001   us/op
[info] MicroBenchClasses.testBAccess    avgt    9    0.003 ±   0.001   us/op

我们看到呼叫时间是相同的,因为在两种情况下我们都在调用方法。我们可以注意到的一件事是B上的吞吐量更高,为什么呢?让我们看一下字节码:

B:

public java.lang.String name();
  Code:
       0: aload_0
       1: getfield      #20                 // Field a:Lcom/testing/SimpleTryExample$A$1;
       4: invokevirtual #22                 // Method com/testing/SimpleTryExample$A$1.name:()Ljava/lang/String;
       7: areturn

B1:

public java.lang.String name();
    Code:
       0: aload_0
       1: getfield      #19                 // Field name:Ljava/lang/String;
       4: areturn

理解getfield为什么慢于invokevirtual并不是一件容易的事,但最终JIT可能会将getter调用内联到name。这表明你不应该把任何事情视为理所当然,以此为基准。

测试代码:

import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations._

/**
  * Created by Yuval.Itzchakov on 19/10/2017.
  */
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3)
@BenchmarkMode(Array(Mode.AverageTime, Mode.Throughput))
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(3)
class MicroBenchClasses {
  class A(val name: String)
  class B(a: A){
    def name: String = a.name
  }

  class B1(a: A){
    val name: String = a.name
  }

  var b: B = _
  var b1: B1 = _
  @Setup
  def setup() = {
    val firstA = new A("yuval")
    val secondA = new A("yuval")

    b = new B(firstA)
    b1 = new B1(secondA)
  }

  @Benchmark
  def testBAccess(): String = {
    b.name
  }

  @Benchmark
  def testB1Access(): String = {
    b1.name
  }
}