Scala'da anlama ve döngüleri nasıl optimize edebilirim?


131

Yani Scala'nın Java kadar hızlı olması gerekiyor. Başlangıçta Java'da ele aldığım Scala'daki bazı Project Euler sorunlarını yeniden ziyaret ediyorum. Spesifik olarak Sorun 5: "1'den 20'ye kadar tüm sayılara eşit olarak bölünebilen en küçük pozitif sayı nedir?"

İşte makinemde tamamlanması 0.7 saniye süren Java çözümüm:

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();
    }
}

İşte Scala'ya 103 saniye süren (147 kat daha uzun!) "Doğrudan çevirim"

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
    }
}

Sonunda, 39 saniye (55 kat daha uzun) süren işlevsel programlama girişimim.

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'i Windows 7 64 bit üzerinde kullanma. Performansı nasıl iyileştiririm? Yanlış bir şey mi yapıyorum? Yoksa Java çok daha mı hızlı?


2
scala kabuğunu kullanarak derler veya yorumlar mısınız?
AhmetB - Google

Bunu yapmanın deneme bölümünü kullanmaktan daha iyi bir yolu var ( İpucu ).
hammar

2
bunu nasıl zamanladığını göstermiyorsun. Sadece runyöntemi zamanlamayı mı denedin ?
Aaron Novstrup

2
@hammar - evet, sadece kalem kağıt yöntemiyle yaptım: yüksek ile başlayan her sayı için asal çarpanları yazın, ardından daha yüksek sayılar için zaten sahip olduğunuz faktörleri işaretleyin, böylece (5 * 2 * 2) ile bitirirsiniz * (19) * (3 * 3) * (17) * (2 * 2) * () * (7) * (13) * () * (11) = 232792560
Luigi Plinge

2
+1 Bu, SO'da haftalardır gördüğüm en ilginç soru (aynı zamanda epeydir gördüğüm en iyi cevaba sahip).
Mia Clarke

Yanıtlar:


111

Bu özel durumda sorun, for-ifadesinin içinden geri dönmenizdir. Bu da, çevreleyen yöntemde yakalanan bir NonLocalReturnException atımına çevrilir. İyileştirici foreach'i ortadan kaldırabilir, ancak fırlatma / yakalama işlemini henüz ortadan kaldıramaz. Ve fırlat / yakala pahalıdır. Ancak, bu tür iç içe geçmiş dönüşler Scala programlarında nadir olduğundan, optimize edici bu durumu henüz ele almadı. Bu sorunu yakında çözeceğini umduğumuz optimize ediciyi geliştirmek için çalışmalar devam ediyor.


9
Geri dönüşün istisna olması oldukça ağır. Eminim bir yerlerde belgelenmiştir, ama anlaşılmaz gizli sihir kokusuna sahiptir. Gerçekten tek yol bu mu?
skrebbel

10
İade, bir kapanışın içinden gerçekleşirse, mevcut en iyi seçenek gibi görünüyor. Dış kapamalardan gelen iadeler, elbette doğrudan bayt kodundaki talimatları döndürmek için çevrilir.
Martin Odersky

1
Eminim bir şeyi gözden kaçırıyorum, ama neden bunun yerine kapalı bir boole bayrağı ve dönüş değeri ayarlamak için bir kapanışın içinden dönüşü derleyip kapatma çağrısı döndükten sonra bunu kontrol etmiyorsunuz?
Luke Hutteman

9
Neden işlevsel algoritması hala 55 kat daha yavaş? Bu kadar korkunç bir performanstan muzdarip gibi görünmüyor
Elijah

4
Şimdi, 2014'te bunu tekrar test ettim ve benim için performans şu şekilde: java -> 0.3s; skala -> 3.6s; skala optimize edilmiş -> 3.5s; skala işlevi -> 4s; 3 yıl öncesine göre çok daha iyi görünüyor, ama ... Yine de fark çok büyük. Daha fazla performans iyileştirmesi bekleyebilir miyiz? Başka bir deyişle, Martin, olası optimizasyonlar için teoride geriye kalan bir şey var mı?
sasha.sochka

80

Sorun, büyük olasılıkla foryöntemde bir kavrayışın kullanılmasıdır isEvenlyDivisible. forEşdeğer bir whiledöngü ile değiştirmek , Java ile performans farkını ortadan kaldırmalıdır.

Java'nın fordöngülerinin aksine , Scala'nın forkavrayışları aslında üst düzey yöntemler için sözdizimsel şekerdir; bu durumda, foreachyöntemi bir Rangenesnede çağırırsınız. Scala forçok geneldir, ancak bazen sancılı performansa neden olur.

-optimizeBayrağı Scala sürüm 2.9'da denemek isteyebilirsiniz . Gözlemlenen performans, kullanımdaki belirli JVM'ye ve JIT optimize edicinin sıcak noktaları belirlemek ve optimize etmek için yeterli "ısınma" süresine sahip olmasına bağlı olabilir.

Posta listesindeki son tartışmalar, Scala ekibinin forbasit durumlarda performansı artırmak için çalıştığını gösteriyor :

Hata izleyicideki sorun şu şekildedir: https://issues.scala-lang.org/browse/SI-4633

28/05 Güncellemesi :

  • Kısa vadeli bir çözüm olarak, ScalaCL eklentisi (alfa) basit Scala döngülerini döngülerin eşdeğerine dönüştürecektir while.
  • Potansiyel bir uzun vadeli çözüm olarak, EPFL ve Stanford ekipleri, çok yüksek performans için "sanal" Scala'nın çalışma anında derlenmesini sağlayan bir proje üzerinde işbirliği yapıyor . Örneğin, birden çok deyimsel işlevsel döngü , çalışma zamanında optimum JVM bayt koduna veya bir GPU gibi başka bir hedefe birleştirilebilir. Sistem, kullanıcı tanımlı DSL'lere ve dönüşümlere izin verecek şekilde genişletilebilir. Check out yayın ve Stanford ders notları . Ön kod, önümüzdeki aylarda yapılması planlanan sürümle birlikte Github'da mevcuttur.

6
Harika, anlama için bir süre döngüsünü değiştirdim ve Java sürümüyle tam olarak aynı hızda (+/- <% 1) çalışıyor. Teşekkürler ... Scala'ya olan inancımı neredeyse bir dakikalığına kaybettim! Şimdi iyi bir işlevsel algoritma üzerinde çalışmalıyım ... :)
Luigi Plinge

24
Kuyruk özyinelemeli işlevlerin de while döngüleri kadar hızlı olduğunu belirtmek gerekir (çünkü her ikisi de çok benzer veya özdeş bayt koduna dönüştürülür).
Rex Kerr

7
Bu beni de bir kez aldı. İnanılmaz yavaşlama nedeniyle bir algoritmayı toplama işlevlerinden yuvalanmış while döngülerine (seviye 6!) Çevirmek zorunda kaldı. Bu, yoğun bir şekilde hedeflenmesi gereken bir şey, imho; İyi bir performansa ihtiyacım olduğunda kullanamazsam (not: hızlı değil) ne işe yarar bir programlama stili?
Raphael

7
O forhalde ne zaman uygundur?
OscarRyz

@OscarRyz - a for in scala, çoğunlukla java'da for (:) gibi davranır.
Mike Axiak

31

Bir takip olarak, -optimize bayrağını denedim ve çalışma süresini 103'ten 76 saniyeye düşürdüm, ancak bu hala Java veya bir while döngüsünden 107 kat daha yavaş.

Sonra "işlevsel" sürüme bakıyordum:

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))
}

ve "forall" dan kısa ve öz bir şekilde nasıl kurtulacağımızı anlamaya çalışmak. Sefil bir şekilde başarısız oldum ve geldim

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))
}

bu sayede kurnaz 5 satırlık çözümüm 12 satıra balonlandı. Ancak bu sürüm 0.71 saniyede , orijinal Java sürümüyle aynı hızda ve "forall" kullanan yukarıdaki sürümden 56 kat daha hızlı (40.2 sn) çalışır! (Bunun Java'dan neden daha hızlı olduğunu öğrenmek için aşağıdaki DÜZENLE'ye bakın)

Açıkçası bir sonraki adımım yukarıdakileri tekrar Java'ya çevirmekti, ancak Java bunu kaldıramaz ve 22000 işareti etrafında n ile bir StackOverflowError atar.

Sonra biraz kafamı kaşıdım ve "while" ı biraz daha fazla kuyruk özyinelemesiyle değiştirdim, bu da birkaç satırı kaydeder, aynı hızda çalışır, ancak kabul edelim, okumak daha kafa karıştırıcı:

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))
}

Böylece Scala'nın kuyruk özyinelemesi günü kazanır, ancak "for" döngüsü (ve "forall" yöntemi) kadar basit bir şeyin esasen bozuk olduğuna ve bunun yerine yetersiz ve ayrıntılı "whiles" veya kuyruk özyinelemesiyle değiştirilmesi gerektiğine şaşırdım. . Scala'yı denememin birçok nedeni kısa sözdiziminden kaynaklanıyor, ancak kodumun 100 kat daha yavaş çalışacak olması iyi değil!

DÜZENLEME : (silindi)

DÜZENLEME DÜZENLEMESİ : 2,5 sn ve 0,7 sn çalışma süreleri arasındaki eski tutarsızlıklar tamamen 32 bit veya 64 bit JVM'lerin kullanılıp kullanılmadığından kaynaklanıyordu. Komut satırından gelen Scala, JAVA_HOME tarafından ayarlanmış olanı kullanırken, Java ne olursa olsun 64-bit kullanır. IDE'lerin kendi ayarları vardır. Burada bazı ölçümler: Eclipse'de Scala yürütme süreleri


1
isDivis metodu şu şekilde yazılabilir: def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1). Scala'da if-else her zaman bir değer döndüren bir ifade olduğuna dikkat edin. Burada return anahtar kelimesine gerek yok.
kiritsuku

3
Son sürümünüz ( P005_V3) aşağıdakileri yazarak daha kısa, daha açıklayıcı ve IMHO daha net hale getirilebilir:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Blaisorblade

@Blaisorblade Hayır. Bu, bayt kodundaki bir while döngüsüne çevirmek için gereken kuyruk özyinelemesini bozar ve bu da yürütmeyi hızlı hale getirir.
gzm0

4
Demek istediğini anlıyorum, ama örneğim && ve || kısa devre değerlendirmesini @tailrec: gist.github.com/Blaisorblade/5672562
Blaisorblade

8

Anlamak için verilecek cevap doğrudur, ancak tüm hikaye değildir. Sen kullanımı o notu dikkat etmelidir returnin isEvenlyDivisibleözgür değildir. for, İçinde return kullanımı , scala derleyicisini yerel olmayan bir dönüş üretmeye zorlar (yani, işlevinin dışına dönmeye).

Bu, döngüden çıkmak için bir istisna kullanılarak yapılır. Aynı şey kendi kontrol soyutlamalarınızı oluşturduğunuzda da olur, örneğin:

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()

Bu yalnızca bir kez "Merhaba" yazar.

Not olduğu returniçinde fooçıkar foo(ki beklediğiniz budur). Parantez ifadesi Eğer imza görebileceğiniz bir fonksiyon değişmezi, olduğundan loop, olmayan bir yerel dönüşü, oluşturmak için bu kuvvetlerin derleyici returnçıkmak için zorlar sizi foo, adil değil body.

Java'da (yani JVM) bu tür davranışları uygulamanın tek yolu bir istisna atmaktır.

Geri dönüyoruz 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 falseDönüş isabet her zaman bu yüzden, çalışma zamanı atmak ve havai GC biraz neden olan bir durum, yakalamak zorundadır, dönüşü olan bir fonksiyon değişmezi olduğunu.


6

forallKeşfettiğim yöntemi hızlandırmanın bazı yolları :

Orijinal: 41,3 s

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

Aralığı önceden örneklemek, böylece her seferinde yeni bir aralık oluşturmuyoruz: 9.0 sn

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

Aralık Yerine Listeye Dönüştürme: 4,8 sn

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

Birkaç başka koleksiyon denedim ama List en hızlıydı (yine de Range ve daha yüksek dereceli işlevden tamamen kaçınmamızdan 7 kat daha yavaş olmasına rağmen).

Scala'da yeniyken, derleyicinin yöntemlerdeki Range değişmezlerini (yukarıdaki gibi) en dış kapsamdaki Range sabitleriyle otomatik olarak değiştirerek kolayca hızlı ve önemli bir performans kazancı sağlayabileceğini tahmin ediyorum. Veya daha iyisi, onları Java'daki Strings literals gibi staj yapın.


dipnot : Diziler, Range ile hemen hemen aynıydı, ancak ilginç bir şekilde, yeni bir forallyöntemin (aşağıda gösterilmiştir) pezevenkleştirilmesi, 64 bitte % 24 ve 32 bitte% 8 daha hızlı yürütmeyle sonuçlandı. Faktör sayısını 20'den 15'e düşürerek hesaplama boyutunu düşürdüğümde fark ortadan kalktı, bu yüzden belki de çöp toplama etkisidir. Nedeni ne olursa olsun, uzun süre tam yük altında çalışırken önemlidir.

List için benzer bir pezevenk de yaklaşık% 10 daha iyi performansla sonuçlandı.

  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

Ben sadece Scala'ya olan inancını bu gibi konularda kaybedebilecek insanlar için, bu tür sorunların hemen hemen tüm işlevsel dillerin performansında ortaya çıktığını söylemek istedim. Haskell'de bir katlamayı optimize ediyorsanız, sık sık yinelemeli bir kuyruk çağrısı için optimize edilmiş döngü olarak yeniden yazmanız gerekecek, aksi takdirde uğraşmanız gereken performans ve bellek sorunlarınız olacaktır.

FP'lerin henüz böyle şeyler hakkında düşünmek zorunda kalmayacağımız noktaya kadar optimize edilmemiş olmasının talihsiz olduğunu biliyorum, ama bu hiç de Scala'ya özgü bir sorun değil.


2

Scala'ya özgü sorunlar zaten tartışılmıştı, ancak asıl sorun, kaba kuvvet algoritması kullanmanın çok da havalı olmamasıdır. Bunu düşünün (orijinal Java kodundan çok daha hızlı):

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
}))

Sorular, belirli bir mantığın performansını diller arasında karşılaştırır. Algoritmanın problem için en uygun olup olmadığı önemsizdir.
smartnut007

1

Project Euler için Scala çözümünde verilen tek astarı deneyin

While döngüsünden uzak olsa da verilen süre en azından sizinkinden daha hızlı .. :)


İşlevsel versiyonuma oldukça benziyor. Benimkini yazabilirsindef r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2) Pavel'inkinden 4 karakter daha kısa . :) Yine de kodumun iyi olduğunu düşünmüyorum - bu soruyu gönderdiğimde toplam 30 satır Scala kodlamıştım.
Luigi Plinge
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.