Scala 中的模式匹配是如何在字节码级别实现的?
Posted
技术标签:
【中文标题】Scala 中的模式匹配是如何在字节码级别实现的?【英文标题】:How is pattern matching in Scala implemented at the bytecode level? 【发布时间】:2010-10-19 17:50:54 【问题描述】:它是像一系列if (x instanceof Foo)
构造,还是别的什么?它对性能有何影响?
例如,给定以下代码(来自Scala By Example 第 46-48 页),eval
方法的等效 Java 代码会是什么样子?
abstract class Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
def eval(e: Expr): Int = e match
case Number(x) => x
case Sum(l, r) => eval(l) + eval(r)
附:我可以阅读 Java 字节码,因此字节码表示对我来说已经足够了,但可能其他读者知道它看起来像 Java 代码会更好。
附言这本书Programming in Scala 是否回答了这个问题以及关于如何实现 Scala 的类似问题?我已经订购了这本书,但它还没有到。
【问题讨论】:
你为什么不直接编译这个例子,然后用一个Java字节码反汇编器来反汇编它呢? 我可能会这样做,除非有人先给出一个好的答案。但现在我想睡一会儿。 ;) 这个问题对其他读者有用! @djondal:最好的表达方式就是支持这个问题:-) 【参考方案1】:扩展@Zifre 的评论:如果您将来阅读本文并且 scala 编译器采用了新的编译策略并且您想知道它们是什么,那么您可以通过以下方式了解它的作用。
将您的match
代码复制并粘贴到一个独立的示例文件中。在该文件上运行 scalac
。然后运行javap -v -c theClassName$.class
。
例如,我将以下内容放入/tmp/question.scala
:
object question
abstract class Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
def eval(e: Expr): Int = e match
case Number(x) => x
case Sum(l, r) => eval(l) + eval(r)
然后我运行scalac question.scala
,它生成了一堆*.class
文件。查了一下,我在question$.class
中找到了匹配语句。 javap -c -v question$.class
输出如下所示。
由于我们正在寻找条件控制流构造,因此了解 java 字节码指令集表明寻找“if”应该是一个不错的起点。
在两个位置,我们在isinstanceof <something>; ifeq <somewhere>
形式上找到一对连续的行,这意味着:如果最近计算的值不是something
的实例,则转到somewhere
。 (ifeq
是 jump if zero
,isinstanceof
给你一个零来表示假。)
如果您遵循控制流,您会发现它与@Jorge Ortiz 给出的答案一致:我们做if (blah isinstanceof something) ... else if (blah isinstanceof somethingelse) ...
。
这是javap -c -v question$.class
的输出:
Classfile /tmp/question$.class
Last modified Nov 20, 2020; size 956 bytes
MD5 checksum cfc788d4c847dad0863a797d980ad2f3
Compiled from "question.scala"
public final class question$
minor version: 0
major version: 50
flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER
this_class: #2 // question$
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 4
Constant pool:
#1 = Utf8 question$
#2 = Class #1 // question$
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 question.scala
#6 = Utf8 MODULE$
#7 = Utf8 Lquestion$;
#8 = Utf8 <clinit>
#9 = Utf8 ()V
#10 = Utf8 <init>
#11 = NameAndType #10:#9 // "<init>":()V
#12 = Methodref #2.#11 // question$."<init>":()V
#13 = Utf8 eval
#14 = Utf8 (Lquestion$Expr;)I
#15 = Utf8 question$Number
#16 = Class #15 // question$Number
#17 = Utf8 n
#18 = Utf8 ()I
#19 = NameAndType #17:#18 // n:()I
#20 = Methodref #16.#19 // question$Number.n:()I
#21 = Utf8 question$Sum
#22 = Class #21 // question$Sum
#23 = Utf8 e1
#24 = Utf8 ()Lquestion$Expr;
#25 = NameAndType #23:#24 // e1:()Lquestion$Expr;
#26 = Methodref #22.#25 // question$Sum.e1:()Lquestion$Expr;
#27 = Utf8 e2
#28 = NameAndType #27:#24 // e2:()Lquestion$Expr;
#29 = Methodref #22.#28 // question$Sum.e2:()Lquestion$Expr;
#30 = NameAndType #13:#14 // eval:(Lquestion$Expr;)I
#31 = Methodref #2.#30 // question$.eval:(Lquestion$Expr;)I
#32 = Utf8 scala/MatchError
#33 = Class #32 // scala/MatchError
#34 = Utf8 (Ljava/lang/Object;)V
#35 = NameAndType #10:#34 // "<init>":(Ljava/lang/Object;)V
#36 = Methodref #33.#35 // scala/MatchError."<init>":(Ljava/lang/Object;)V
#37 = Utf8 this
#38 = Utf8 e
#39 = Utf8 Lquestion$Expr;
#40 = Utf8 x
#41 = Utf8 I
#42 = Utf8 l
#43 = Utf8 r
#44 = Utf8 question$Expr
#45 = Class #44 // question$Expr
#46 = Methodref #4.#11 // java/lang/Object."<init>":()V
#47 = NameAndType #6:#7 // MODULE$:Lquestion$;
#48 = Fieldref #2.#47 // question$.MODULE$:Lquestion$;
#49 = Utf8 question
#50 = Class #49 // question
#51 = Utf8 Sum
#52 = Utf8 Expr
#53 = Utf8 Number
#54 = Utf8 Code
#55 = Utf8 LocalVariableTable
#56 = Utf8 LineNumberTable
#57 = Utf8 StackMapTable
#58 = Utf8 SourceFile
#59 = Utf8 InnerClasses
#60 = Utf8 ScalaInlineInfo
#61 = Utf8 Scala
public static final question$ MODULE$;
descriptor: Lquestion$;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
public static ;
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: new #2 // class question$
3: invokespecial #12 // Method "<init>":()V
6: return
public int eval(question$Expr);
descriptor: (Lquestion$Expr;)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=9, args_size=2
0: aload_1
1: astore_2
2: aload_2
3: instanceof #16 // class question$Number
6: ifeq 27
9: aload_2
10: checkcast #16 // class question$Number
13: astore_3
14: aload_3
15: invokevirtual #20 // Method question$Number.n:()I
18: istore 4
20: iload 4
22: istore 5
24: goto 69
27: aload_2
28: instanceof #22 // class question$Sum
31: ifeq 72
34: aload_2
35: checkcast #22 // class question$Sum
38: astore 6
40: aload 6
42: invokevirtual #26 // Method question$Sum.e1:()Lquestion$Expr;
45: astore 7
47: aload 6
49: invokevirtual #29 // Method question$Sum.e2:()Lquestion$Expr;
52: astore 8
54: aload_0
55: aload 7
57: invokevirtual #31 // Method eval:(Lquestion$Expr;)I
60: aload_0
61: aload 8
63: invokevirtual #31 // Method eval:(Lquestion$Expr;)I
66: iadd
67: istore 5
69: iload 5
71: ireturn
72: new #33 // class scala/MatchError
75: dup
76: aload_2
77: invokespecial #36 // Method scala/MatchError."<init>":(Ljava/lang/Object;)V
80: athrow
LocalVariableTable:
Start Length Slot Name Signature
0 81 0 this Lquestion$;
0 81 1 e Lquestion$Expr;
20 61 4 x I
47 34 7 l Lquestion$Expr;
54 27 8 r Lquestion$Expr;
LineNumberTable:
line 6: 0
line 7: 2
line 8: 27
line 6: 69
StackMapTable: number_of_entries = 3
frame_type = 252 /* append */
offset_delta = 27
locals = [ class question$Expr ]
frame_type = 254 /* append */
offset_delta = 41
locals = [ top, top, int ]
frame_type = 248 /* chop */
offset_delta = 2
SourceFile: "question.scala"
InnerClasses:
public static #51= #22 of #50; // Sum=class question$Sum of class question
public static abstract #52= #45 of #50; // Expr=class question$Expr of class question
public static #53= #16 of #50; // Number=class question$Number of class question
ScalaInlineInfo: length = 0xE (unknown attribute)
01 01 00 02 00 0A 00 09 01 00 0D 00 0E 01
Scala: length = 0x0 (unknown attribute)
【讨论】:
【参考方案2】:从 2.8 版开始,Scala 有了 @switch 注释。目标是确保将模式匹配编译成tableswitch or lookupswitch,而不是一系列条件if
语句。
【讨论】:
什么时候选择@switch而不是常规? 使用@switch
比常规模式匹配更有效。因此,如果所有情况都包含常量值,则应始终使用@switch
(因为字节码实现将与 java switch
相同,而不是许多 if-else)【参考方案3】:
詹姆斯(上图)说得最好。但是,如果您好奇,查看反汇编的字节码总是一个很好的练习。您还可以使用-print
选项调用scalac
,这将打印您的程序并删除所有Scala 特定的功能。它基本上是 Scala 中的 Java。这是您提供的代码 sn-p 的相关 scalac -print
输出:
def eval(e: Expr): Int =
<synthetic> val temp10: Expr = e;
if (temp10.$isInstanceOf[Number]())
temp10.$asInstanceOf[Number]().n()
else
if (temp10.$isInstanceOf[Sum]())
<synthetic> val temp13: Sum = temp10.$asInstanceOf[Sum]();
Main.this.eval(temp13.e1()).+(Main.this.eval(temp13.e2()))
else
throw new MatchError(temp10)
;
【讨论】:
【参考方案4】:可以使用反汇编程序探索低级别,但简短的回答是它是一堆 if/else,其中谓词取决于模式
case Sum(l,r) // instance of check followed by fetching the two arguments and assigning to two variables l and r but see below about custom extractors
case "hello" // equality check
case _ : Foo // instance of check
case x => // assignment to a fresh variable
case _ => // do nothing, this is the tail else on the if/else
您可以使用诸如“case Foo(45, x)”之类的模式或模式和组合进行更多操作,但通常这些只是我刚才描述的逻辑扩展。模式也可以有守卫,这是对谓词的附加约束。在某些情况下,编译器可以优化模式匹配,例如,当案例之间有一些重叠时,它可能会合并一些事情。高级模式和优化是编译器中一个活跃的工作领域,所以如果字节码在当前和未来版本的 Scala 中比这些基本规则有显着改进,请不要感到惊讶。
除此之外,您可以编写自己的自定义提取器来补充或代替 Scala 用于案例类的默认提取器。如果你这样做了,那么模式匹配的成本就是提取器所做的任何事情的成本。在http://lamp.epfl.ch/~emir/written/MatchingObjectsWithPatterns-TR.pdf中找到了一个很好的概述
【讨论】:
我相信这是当前链接:infoscience.epfl.ch/record/98468/files/…以上是关于Scala 中的模式匹配是如何在字节码级别实现的?的主要内容,如果未能解决你的问题,请参考以下文章