วิธีเพิ่มประสิทธิภาพสำหรับความเข้าใจและการวนซ้ำใน Scala


131

ดังนั้น Scala ควรจะเร็วเท่ากับ Java ฉันกำลังแก้ไขปัญหาProject Eulerบางอย่างใน Scala ที่ฉันได้แก้ไขใน Java ปัญหาเฉพาะที่ 5: "จำนวนบวกที่น้อยที่สุดที่หารด้วยตัวเลขทั้งหมดตั้งแต่ 1 ถึง 20 เท่ากันคืออะไร"

นี่คือโซลูชัน Java ของฉันซึ่งใช้เวลา 0.7 วินาทีในการดำเนินการบนเครื่องของฉัน:

public class P005_evenly_divisible implements Runnable{
    final int t = 20;

    public void run() {
        int i = 10;
        while(!isEvenlyDivisible(i, t)){
            i += 2;
        }
        System.out.println(i);
    }

    boolean isEvenlyDivisible(int a, int b){
        for (int i = 2; i <= b; i++) {
            if (a % i != 0) 
                return false;
        }
        return true;
    }

    public static void main(String[] args) {
        new P005_evenly_divisible().run();
    }
}

นี่คือ "การแปลโดยตรง" ของฉันเป็น Scala ซึ่งใช้เวลา 103 วินาที (นานกว่า 147 เท่า!)

object P005_JavaStyle {
    val t:Int = 20;
    def run {
        var i = 10
        while(!isEvenlyDivisible(i,t))
            i += 2
        println(i)
    }
    def isEvenlyDivisible(a:Int, b:Int):Boolean = {
        for (i <- 2 to b)
            if (a % i != 0)
                return false
        return true
    }
    def main(args : Array[String]) {
        run
    }
}

ในที่สุดนี่คือความพยายามของฉันในการเขียนโปรแกรมเชิงฟังก์ชันซึ่งใช้เวลา 39 วินาที (นานกว่า 55 เท่า)

object P005 extends App{
    def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
    def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
    println (find (2))
}

ใช้ Scala 2.9.0.1 บน Windows 7 64-bit ฉันจะปรับปรุงประสิทธิภาพได้อย่างไร ฉันทำอะไรผิดหรือเปล่า? หรือ Java เร็วกว่ามาก?


2
คุณรวบรวมหรือตีความโดยใช้เชลล์สกาล่าหรือไม่?
AhmetB - Google

มีวิธีที่ดีกว่าการใช้การแบ่งการทดลอง ( คำแนะนำ )
hammar

2
คุณไม่ได้แสดงให้เห็นว่าคุณกำหนดเวลาอย่างไร คุณลองจับเวลาrunวิธีนี้หรือไม่?
Aaron Novstrup

2
@hammar - ใช่มันเป็นวิธีปากกาและกระดาษ: เขียนปัจจัยที่สำคัญสำหรับแต่ละหมายเลขที่เริ่มต้นด้วยสูงจากนั้นขีดฆ่าปัจจัยที่คุณมีอยู่แล้วเพื่อให้ได้ตัวเลขที่สูงขึ้นดังนั้นคุณจึงจบด้วย (5 * 2 * 2) * (19) * (3 * 3) * (17) * (2 * 2) * () * (7) * (13) * () * (11) = 232792560
Luigi Plinge

2
+1 นี่เป็นคำถามที่น่าสนใจที่สุดที่ฉันเคยเห็นใน SO SO (ซึ่งเป็นคำตอบที่ดีที่สุดที่ฉันเคยเห็นในช่วงหนึ่ง)
Mia Clarke

คำตอบ:


111

ปัญหาในกรณีนี้คือคุณส่งคืนจากภายใน for-expression ในทางกลับกันจะถูกแปลเป็นการโยน NonLocalReturnException ซึ่งถูกจับที่วิธีการปิดล้อม เครื่องมือเพิ่มประสิทธิภาพสามารถกำจัด foreach ได้ แต่ยังไม่สามารถกำจัดการโยน / จับได้ และโยน / จับมีราคาแพง แต่เนื่องจากผลตอบแทนที่ซ้อนกันดังกล่าวหายากในโปรแกรม Scala เครื่องมือเพิ่มประสิทธิภาพจึงยังไม่ได้จัดการกับกรณีนี้ มีการปรับปรุงเครื่องมือเพิ่มประสิทธิภาพซึ่งหวังว่าจะแก้ปัญหานี้ได้ในไม่ช้า


9
ค่อนข้างหนักที่ผลตอบแทนจะกลายเป็นข้อยกเว้น ฉันแน่ใจว่ามันถูกบันทึกไว้ที่ไหนสักแห่ง แต่มันมีความมหัศจรรย์ซ่อนเร้นที่ไม่อาจเข้าใจได้ นั่นเป็นวิธีเดียวจริงๆหรือ?
skrebbel

10
หากผลตอบแทนเกิดขึ้นจากภายในการปิดดูเหมือนว่าจะเป็นตัวเลือกที่ดีที่สุด แน่นอนว่าผลตอบแทนจากการปิดภายนอกได้รับการแปลโดยตรงเพื่อส่งคืนคำแนะนำใน bytecode
Martin Odersky

1
ฉันแน่ใจว่าฉันกำลังมองข้ามบางสิ่งอยู่ แต่ทำไมไม่รวบรวมผลตอบแทนจากภายในการปิดแทนเพื่อตั้งค่าสถานะบูลีนที่แนบมาและค่าส่งคืนและตรวจสอบว่าหลังจากการปิดการโทรกลับมาแล้ว?
Luke Hutteman

9
ทำไมอัลกอริทึมการทำงานของเขายังช้ากว่า 55 เท่า? ดูเหมือนว่าจะไม่ได้รับผลกระทบจากการแสดงที่น่ากลัวเช่นนี้
Elijah

4
ตอนนี้ในปี 2014 ฉันได้ทดสอบสิ่งนี้อีกครั้งและสำหรับฉันประสิทธิภาพดังต่อไปนี้: java -> 0.3s; สกาลา -> 3.6 วินาที; ปรับให้เหมาะสมสกาลา -> 3.5 วินาที; ฟังก์ชั่น scala -> 4s; ดูดีกว่าเมื่อ 3 ปีก่อนมาก แต่ ... ความแตกต่างยังใหญ่เกินไป เราสามารถคาดหวังการปรับปรุงประสิทธิภาพเพิ่มเติมได้หรือไม่? กล่าวอีกนัยหนึ่งคือมาร์ตินในทางทฤษฎีมีอะไรเหลือสำหรับการเพิ่มประสิทธิภาพที่เป็นไปได้หรือไม่?
sasha.sochka

80

ปัญหาคือส่วนใหญ่มีแนวโน้มการใช้งานที่เข้าใจในวิธีการfor isEvenlyDivisibleแทนที่forด้วยสิ่งที่เทียบเท่าwhileลูปควรกำจัดความแตกต่างของประสิทธิภาพกับ Java

ในทางตรงกันข้ามกับการforวนซ้ำของ Java forความเข้าใจของ Scala นั้นเป็นน้ำตาลที่เป็นประโยคสำหรับวิธีการลำดับที่สูงกว่า ในกรณีนี้คุณกำลังเรียกใช้foreachเมธอดบนRangeวัตถุ Scala ของforเป็นเรื่องธรรมดามาก แต่บางครั้งก็นำไปสู่การแสดงที่เจ็บปวด

คุณอาจต้องการลองใช้ไฟล์ -optimizeแฟล็กใน Scala เวอร์ชัน 2.9 ประสิทธิภาพที่สังเกตได้อาจขึ้นอยู่กับ JVM ที่ใช้อยู่และเครื่องมือเพิ่มประสิทธิภาพ JIT ที่มีเวลา "อุ่นเครื่อง" เพียงพอในการระบุและเพิ่มประสิทธิภาพฮอตสปอต

การสนทนาล่าสุดเกี่ยวกับรายชื่อส่งเมลระบุว่าทีม Scala กำลังดำเนินการปรับปรุงforประสิทธิภาพในกรณีง่ายๆ:

นี่คือปัญหาในตัวติดตามข้อบกพร่อง: https://issues.scala-lang.org/browse/SI-4633

อัปเดต 5/28 :

  • ในการแก้ปัญหาระยะสั้นปลั๊กอินScalaCL (อัลฟา) จะเปลี่ยน Scala loops แบบธรรมดาให้เทียบเท่ากับwhileลูป
  • ในฐานะโซลูชันระยะยาวที่เป็นไปได้ทีมจาก EPFL และ Stanford กำลังร่วมมือกันในโครงการที่เปิดใช้งานการรวบรวมScala แบบ "เสมือน"แบบรันไทม์เพื่อประสิทธิภาพที่สูงมาก ตัวอย่างเช่นลูปการทำงานหลายสำนวนสามารถหลอมรวมกันในขณะทำงานเป็นรหัสไบต์ JVM ที่เหมาะสมที่สุดหรือไปยังเป้าหมายอื่นเช่น GPU ระบบสามารถขยายได้ทำให้ผู้ใช้กำหนด DSL และการแปลง ตรวจสอบสิ่งพิมพ์และ Stanford บันทึกหลักสูตร โค้ดเบื้องต้นพร้อมใช้งานบน Github และจะวางจำหน่ายในอีกไม่กี่เดือนข้างหน้า

6
เยี่ยมมากฉันแทนที่สำหรับความเข้าใจด้วย while loop และมันทำงานด้วยความเร็วเดียวกัน (+/- <1%) เหมือนกับเวอร์ชัน Java ขอบคุณ ... ฉันเกือบจะหมดศรัทธาใน Scala ไปชั่วขณะ! ตอนนี้ต้องทำงานกับอัลกอริธึมที่ใช้งานได้ดี ... :)
Luigi Plinge

24
เป็นที่น่าสังเกตว่าฟังก์ชัน tail-recursive นั้นเร็วพอ ๆ กับในขณะที่ลูป (เนื่องจากทั้งสองถูกแปลงเป็น bytecode ที่เหมือนกันหรือเหมือนกันมาก)
Rex Kerr

7
นี่ก็มีฉันครั้งหนึ่งเหมือนกัน ต้องแปลอัลกอริทึมจากการใช้ฟังก์ชั่นการรวบรวมเป็นซ้อนกันในขณะที่ลูป (ระดับ 6!) เนื่องจากการทำงานช้าลงอย่างไม่น่าเชื่อ นี่คือสิ่งที่ต้องกำหนดเป้าหมายอย่างหนัก imho; รูปแบบการเขียนโปรแกรมที่ดีคืออะไรถ้าฉันไม่สามารถใช้งานได้เมื่อฉันต้องการประสิทธิภาพที่ดี (หมายเหตุ: ไม่เร็วมาก)
Raphael

7
เมื่อใดจึงforเหมาะสม?
OscarRyz

@OscarRyz - สำหรับใน scala จะทำงานเป็น for (:) ใน java ส่วนใหญ่
Mike Axiak

31

จากการติดตามผลฉันลองใช้แฟล็ก -optimize และลดเวลาในการทำงานจาก 103 เป็น 76 วินาที แต่ก็ยังช้ากว่า Java 107 เท่าหรือในขณะที่วนซ้ำ

จากนั้นฉันกำลังดูเวอร์ชัน "ใช้งานได้":

object P005 extends App{
  def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

และพยายามหาวิธีกำจัด "forall" อย่างรวบรัด ฉันล้มเหลวอย่างอนาถและคิดขึ้นมา

object P005_V2 extends App {
  def isDivis(x:Int):Boolean = {
    var i = 1
    while(i <= 20) {
      if (x % i != 0) return false
      i += 1
    }
    return true
  }
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

โดยวิธีแก้ปัญหา 5 บรรทัดที่มีไหวพริบของฉันได้แบ่งเป็น 12 บรรทัด อย่างไรก็ตามเวอร์ชันนี้ทำงานใน0.71 วินาทีความเร็วเท่ากับเวอร์ชัน Java ดั้งเดิมและเร็วกว่าเวอร์ชันด้านบน 56 เท่าโดยใช้ "forall" (40.2 วินาที)! (ดูแก้ไขด้านล่างสำหรับสาเหตุที่เร็วกว่า Java)

เห็นได้ชัดว่าขั้นตอนต่อไปของฉันคือการแปลด้านบนกลับเป็น Java แต่ Java ไม่สามารถจัดการได้และพ่น StackOverflowError ด้วย n รอบ ๆ เครื่องหมาย 22000

จากนั้นฉันเกาหัวของฉันเล็กน้อยและแทนที่ "while" ด้วยการเรียกซ้ำหางอีกเล็กน้อยซึ่งช่วยประหยัดสองสามบรรทัดวิ่งเร็วพอ ๆ กัน แต่มาดูกันว่าอ่านแล้วสับสนกว่า:

object P005_V3 extends App {
  def isDivis(x:Int, i:Int):Boolean = 
    if(i > 20) true
    else if(x % i != 0) false
    else isDivis(x, i+1)

  def find(n:Int):Int = if (isDivis(n, 2)) n else find (n+2)
  println (find (2))
}

ดังนั้นการวนซ้ำหางของ Scala จึงชนะในวันนี้ แต่ฉันประหลาดใจที่บางสิ่งที่เรียบง่ายอย่างการวนซ้ำ "for" (และวิธี "forall") นั้นเสียไปแล้วและต้องถูกแทนที่ด้วย "whiles" ที่ไม่สง่างาม . เหตุผลหลายอย่างที่ฉันลองใช้ Scala นั้นเป็นเพราะไวยากรณ์ที่กระชับ แต่ก็ไม่ดีเลยถ้าโค้ดของฉันจะทำงานช้าลง 100 เท่า!

แก้ไข : (ลบ)

การแก้ไข : ความคลาดเคลื่อนในอดีตระหว่างเวลาทำงาน 2.5 วินาทีและ 0.7 วินาทีเกิดจากการใช้ JVM แบบ 32 บิตหรือ 64 บิต Scala จากบรรทัดคำสั่งใช้สิ่งที่กำหนดโดย JAVA_HOME ในขณะที่ Java ใช้ 64 บิตหากมีโดยไม่คำนึงถึง IDE มีการตั้งค่าของตนเอง การวัดบางส่วนที่นี่:เวลาดำเนินการ Scala ใน Eclipse


1
def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)isDivis-วิธีสามารถเขียนเป็น: สังเกตว่าใน Scala if-else คือนิพจน์ที่ส่งคืนค่าเสมอ ไม่ต้องใช้คีย์เวิร์ด return ที่นี่
kiritsuku

3
เวอร์ชันล่าสุดของคุณ ( P005_V3) สามารถทำให้สั้นลงประกาศมากขึ้นและ IMHO ชัดเจนขึ้นโดยการเขียน:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Blaisorblade

@Blaisorblade ไม่สิ่งนี้จะทำลาย tail-recursiveness ซึ่งจำเป็นในการแปลเป็น while-loop ใน bytecode ซึ่งจะทำให้การดำเนินการรวดเร็ว
gzm0

4
ฉันเห็นประเด็นของคุณ แต่ตัวอย่างของฉันยังคงวนซ้ำตั้งแต่ && และ || ใช้การประเมินการลัดวงจรตามที่ยืนยันโดยใช้ @tailrec: gist.github.com/Blaisorblade/5672562
Blaisorblade

8

คำตอบเกี่ยวกับเพื่อความเข้าใจนั้นถูกต้อง แต่ไม่ใช่เรื่องราวทั้งหมด คุณควรทราบว่าการใช้งานreturnในisEvenlyDivisibleนั้นไม่ฟรี การใช้ผลตอบแทนภายในforคอมไพเลอร์สกาลาจะสร้างผลตอบแทนที่ไม่ใช่ภายในเครื่อง (เช่นการส่งคืนนอกฟังก์ชัน)

สิ่งนี้ทำได้โดยใช้ข้อยกเว้นเพื่อออกจากลูป สิ่งเดียวกันนี้จะเกิดขึ้นหากคุณสร้างนามธรรมการควบคุมของคุณเองตัวอย่างเช่น:

def loop[T](times: Int, default: T)(body: ()=>T) : T = {
    var count = 0
    var result: T = default
    while(count < times) {
        result = body()
        count += 1
    }
    result
}

def foo() : Int= {
    loop(5, 0) {
        println("Hi")
        return 5
    }
}

foo()

สิ่งนี้จะพิมพ์ "Hi" เพียงครั้งเดียว

โปรดทราบว่าreturnในfooการออกfoo(ซึ่งเป็นสิ่งที่คุณคาดหวัง) เนื่องจากนิพจน์วงเล็บเป็นฟังก์ชันลิเทอรัลซึ่งคุณสามารถเห็นได้ในลายเซ็นของloopสิ่งนี้บังคับให้คอมไพเลอร์สร้างผลตอบแทนที่ไม่ใช่โลคัลนั่นคือreturnบังคับให้คุณออกfooไม่ใช่แค่body .

ใน Java (เช่น JVM) วิธีเดียวที่จะใช้พฤติกรรมดังกล่าวคือการยกเว้น

กลับไปที่isEvenlyDivisible:

def isEvenlyDivisible(a:Int, b:Int):Boolean = {
  for (i <- 2 to b) 
    if (a % i != 0) return false
  return true
}

if (a % i != 0) return falseเป็นตัวอักษรฟังก์ชั่นที่มีผลตอบแทนดังนั้นทุกครั้งที่กลับมาจะตีรันไทม์ที่มีการโยนและจับข้อยกเว้นซึ่งเป็นสาเหตุให้ไม่น้อยของ GC เหนือศีรษะ


6

บางวิธีในการเร่งความเร็วforallวิธีที่ฉันค้นพบ:

ต้นฉบับ: 41.3 วิ

def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}

การสร้างอินสแตนซ์ล่วงหน้าช่วงดังนั้นเราจึงไม่สร้างช่วงใหม่ทุกครั้ง: 9.0 วินาที

val r = (1 to 20)
def isDivis(x:Int) = r forall {x % _ == 0}

การแปลงเป็นรายการแทนที่จะเป็นช่วง: 4.8 วินาที

val rl = (1 to 20).toList
def isDivis(x:Int) = rl forall {x % _ == 0}

ฉันลองใช้คอลเลคชันอื่น ๆ แต่ List เร็วที่สุด (แม้ว่าจะยังช้ากว่า 7 เท่าหากเราหลีกเลี่ยงฟังก์ชัน Range และลำดับที่สูงกว่าโดยสิ้นเชิง)

ในขณะที่ฉันยังใหม่กับ Scala แต่ฉันเดาว่าคอมไพเลอร์สามารถใช้งานการเพิ่มประสิทธิภาพที่รวดเร็วและมีนัยสำคัญได้อย่างง่ายดายเพียงแค่แทนที่ Range literals ในวิธีการ (ตามด้านบน) โดยอัตโนมัติด้วยค่าคงที่ของ Range ในขอบเขตนอกสุด หรือดีกว่านั้นให้ฝึกงานเช่น Strings literals ใน Java


เชิงอรรถ : อาร์เรย์นั้นใกล้เคียงกับ Range แต่ที่น่าสนใจคือการใช้forallวิธีใหม่(แสดงด้านล่าง) ส่งผลให้การดำเนินการเร็วขึ้น 24% บน 64 บิตและเร็วขึ้น 8% สำหรับ 32 บิต เมื่อฉันลดขนาดการคำนวณโดยลดจำนวนปัจจัยจาก 20 เหลือ 15 ความแตกต่างก็หายไปดังนั้นอาจเป็นผลจากการเก็บขยะ ไม่ว่าจะเกิดจากสาเหตุใดก็สำคัญเมื่อทำงานภายใต้ภาระงานเต็มที่เป็นระยะเวลานาน

แมงดาที่คล้ายกันสำหรับ List ยังส่งผลให้ประสิทธิภาพดีขึ้นประมาณ 10%

  val ra = (1 to 20).toArray
  def isDivis(x:Int) = ra forall2 {x % _ == 0}

  case class PimpedSeq[A](s: IndexedSeq[A]) {
    def forall2 (p: A => Boolean): Boolean = {      
      var i = 0
      while (i < s.length) {
        if (!p(s(i))) return false
        i += 1
      }
      true
    }    
  }  
  implicit def arrayToPimpedSeq[A](in: Array[A]): PimpedSeq[A] = PimpedSeq(in)  

3

ฉันแค่อยากจะแสดงความคิดเห็นสำหรับผู้ที่อาจสูญเสียความเชื่อมั่นใน Scala เกี่ยวกับปัญหาเช่นนี้ว่าปัญหาประเภทนี้เกิดขึ้นจากการทำงานของภาษาที่ใช้งานได้ทั้งหมด หากคุณกำลังเพิ่มประสิทธิภาพการพับใน Haskell คุณมักจะต้องเขียนซ้ำเป็นลูปที่ปรับให้เหมาะกับการโทรหางซ้ำไม่เช่นนั้นคุณจะมีปัญหาด้านประสิทธิภาพและหน่วยความจำที่ต้องโต้แย้ง

ฉันรู้ว่าโชคไม่ดีที่ FPs ยังไม่ได้รับการปรับให้เหมาะสมจนถึงจุดที่เราไม่ต้องคิดถึงเรื่องแบบนี้ แต่นี่ไม่ใช่ปัญหาเฉพาะสำหรับ Scala


2

มีการพูดถึงปัญหาเฉพาะของ Scala แล้ว แต่ปัญหาหลักคือการใช้อัลกอริธึม brute-force นั้นไม่ค่อยดีนัก พิจารณาสิ่งนี้ (เร็วกว่าโค้ด Java ดั้งเดิมมาก):

def gcd(a: Int, b: Int): Int = {
    if (a == 0)
        b
    else
        gcd(b % a, a)
}
print (1 to 20 reduce ((a, b) => {
  a / gcd(a, b) * b
}))

คำถามจะเปรียบเทียบประสิทธิภาพของตรรกะเฉพาะในภาษาต่างๆ อัลกอริทึมที่เหมาะสมสำหรับปัญหานั้นไม่มีสาระสำคัญหรือไม่
smartnut007

1

ลองใช้ซับเดียวที่กำหนดในโซลูชันScala สำหรับ Project Euler

เวลาที่ให้นั้นเร็วกว่าของคุณเป็นอย่างน้อยแม้ว่าจะอยู่ไกลจาก while loop .. :)


ค่อนข้างคล้ายกับเวอร์ชันที่ใช้งานได้ของฉัน คุณสามารถเขียนของฉันเป็นdef r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)ซึ่งสั้นกว่า Pavel 4 ตัวอักษร :) อย่างไรก็ตามฉันไม่ได้แสร้งทำเป็นว่ารหัสของฉันเป็นสิ่งที่ดี - เมื่อฉันโพสต์คำถามนี้ฉันได้เขียนโค้ด Scala ทั้งหมดประมาณ 30 บรรทัด
Luigi Plinge
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.