Ilus vs. kiire kood II: Ruby, Scala, Java

Niisis, nagu eelmises postituses sai lubatud, testin seekord lihtsat ringsõltuvuste leidmise algoritmi kolmes erinevas keeles: Ruby, Scala ja Java. Lisaks otsustasin veel testi lisada JRuby — skriptis midagi ei muutu, tekib lihtsalt võrdlusmoment JVM-l jooksva JRuby ja tavalise Ruby vahel.

Kindlasti on iga allpool toodud skripti asemel võimalik samas keeles parem lahendus kirjutada. Ideaalset programmi pole niikuinii olemas — alati on tegu mingi kompromissiga. Antud juhul arvestatakse näiteks järgmisi nõudmisi: kood olgu varem ära toodud algoritmile vastav, keele iseärasusi arvestav, optimiseerimise eesmärgil enda loodud andmestruktuure mitte kasutav, kuna eeldatavasti on tegu eraldiseisva, lightweight ja harva käivitatava skriptiga ja kood võiks jääda võimalikult lihtsaks.


Alustan Rubyst. Võtsin AK skripti ja kohandasin seda veidi, et see oleks paremini võrreldav Scala ja Java koodiga. Ise olen Ruby suhtes üsna võhik — loodan, et väga puusse ei pannud. Esialgne skript, mis käis paar tundi, jäi võrdlusest välja kuna ma ei viitsinud seda lihtsalt korduvalt jooksutada. Skript, mida testisin, on järgmine:

require 'hpricot'

def find_circular(doc, bean, path)
  id = bean.attributes['id']
  if path.include? id
    (path + [id]).each_with_index{|bean_name, i| print "" + (i>0 ? ' -> "': '"') + bean_name + '"' }
    puts ";"
  else
    (bean / 'property').each { |a|
      ref = a.attributes['ref']
      if !ref.nil?
        doc.search("beans/bean[@id='" + ref + "']").each { |bean|
          find_circular(doc, bean, path + [id])
        }
      end
    }
  end
end

doc = Hpricot.XML(open( "spring-beans.xml" ))

puts "digraph {[concentrate=true]"
doc.search('beans/bean').each { |bean|
  find_circular(doc, bean, [])
}
puts "}"

Järgmiseks Scala. Scala puhul katsetasin kõige rohkem eri variante ja skript läbis mitu iteratrsiooni, kuni jõudsin enda arvates üsna hea lahenduseni, mis on päris lähedal ülaltoodud pseudokoodile (aga see võib tuleneda ka sellest, et pseudokoodi kirjutasin tegelikult hiljem ja Scala kood kindlasti mõjutas seda).

val doc = xml.XML.loadFile("spring-beans.xml")

def findCircular(lastBean: xml.Node, path: List[String]): Unit = {
  if (path contains (lastBean\"@id" text)) {
    println(((lastBean\"@id" text) :: path).reverse.mkString("\"", "\" -> \"", "\"") + ";")
  } else for {
    ref <- lastBean\"property"\\\"@ref" if !(ref isEmpty)
    bean <- doc\"bean" if (bean\"@id") == ref
  } findCircular(bean, (lastBean\"@id" text) :: path)
}

println("digraph {[concentrate=true]")
for (bean <- doc\"bean") {
  findCircular(bean, Nil)
}
println("}")

Javast on aga lausa kaks erinevat programmi. Kuna Java kood on üsna pikk, siis ma ei hakka nende sisu siin näitama vaid annan lingid failidele: üks kasutab ainult DOM API’t, teine nii DOM’i kui XPath‘i. Miks ma kaks varianti tegin, on üsna varsti näha. Aga vaadates seda koodi on üsna selge, et Java ei ole skriptimiseks eriti sobiv keel. Kood on täis accidental complexity‘t. XPath’i variant on võib-olla natuke arusaadavam kuna vajab vähem tsükleid. Kindlasti annaks seda ka normaalsemaks kirjutada, luues näiteks spetsiaalselt sellise koodi lihtsustamiseks mõeldud teegi, aga imesid ei tasu loota. IDE teeb muidugi selle koodi kribamise lihtsamaks, aga “väikseid” skripte ei taha ju alati IDE’s kirjutada.

Ruby ja Scala kood on üsna sarnane ja Javaga võrreldes minu arvates oluliselt lihtsamini jälgitav. Mõlemad vastavad enam-vähem eespool toodud pseudokoodile. Scala juures meeldib mulle see, et saab kirjutada natuke kompaktsemalt, aga võib-olla mõne jaoks on Ruby jälle loetavam. Huvitav on minu arvates see, et Scala saavutab Rubyga sarnase abstraktsiooni taseme, kuid on seejuures isegi vaat et rohkem type-safe kui Java — Java vajab näiteks XPath avaldiste juures cast‘e, Scalas ei läinud neid üldse vaja. Tuleb välja, et skriptimiskeeled ei peagi tingimata dünaamiliste tüüpidega olema! Staatilised tüübid ei anna sellise väikse skripti puhul küll erilist eelist, aga nagu kohe näha, siis tundub, et jõudlusele mõjuvad nad hästi.

Vaatakski siis nüüd jõudlust ka. Kuna numbrid tulid sellised, et väga suur täpsust ma võrdluses vajalikuks ei pidanud, siis mõõtsin umbes sekundi täpsusega, käivitades Windows XP batch failis käsu time nii enne kui pärast programmi käivitust ja võrreldes alguse ja lõpu aegasid. Kõiki skripte käivitasin mitu korda, iga kord tuli enam-vähem sama tulemus, väikeste kõikumistega. Väikse faili puhul, mis sai alguses näitena toodud, said kõik keeled alla sekundiga hakkama, ma ei hakanudki täpselt mõõtma. Suure faili puhul sain järgmised tulemused, lisan ka subjektiivse hinnangu igale keelele selle ülesande täitmise osas:

Ruby 5 min 48 sek ilus kood, jube aeglane
JRuby 4 min 52 sek
Scala 3 sek ilus kood, kiire
Java (DOM + XPath) 44 sek kole kood, suht aeglane
Java (DOM) 4 sek väga kole kood, kiire

Natuke üllatavad numbrid! Minu eelarvamus oli, et Java võiks Scalast natuke parem olla. Aga millest need vahed tulevad? Kõige rohkem peaks siin olema mängus XML teekide kiirus. JRuby on natuke kiirem kui Ruby, aga Ruby koha pealt ei ole mul infot kas Hpricot või keele enda kiirus mängib suuremat rolli. Kuna Java variant XPath’iga on tunduvalt aeglasem kui ilma, siis XPath tundubki üldiselt asja kõvasti mõjutavat. Scala pääseb siin sellega, et ta ei kasuta tegelikult XPath’i, vaid lihtsalt pakub sarnast API’t. Java DOM läheneb Scala kiirusele, aga jääb siiski alla. Siin võib mingi väike mõju olla Scala List tüübil, mis on antud algoritmis kasutatud rekursiooni jaoks võib-olla, et isegi kõige sobivam andmetüüp üldse. Aga suurem vahe tuleb pigem sellest, et Scala kasutab sisemiselt hoopiski SAX‘i, mitte DOM’i. Alguses mõtlesin selles testis kaasata ka Java SAX’i variandi, aga kuna postitus on niigi pikaks veninud, siis see jääb järgmisesse osasse, mis tuleb loodetavasti paari nädala jooksul. Proovin SAX’i arvatavasti ka Ruby’s ja Scala’s, aga siis juba natuke efektiivsema algoritmiga, kuna praegune SAX’i jaoks eriti ei sobi.

Käesolevas testis aga on näitajad kokkuvõttes selgelt Scala kasuks ja paistab, et selliste ülesannete jaoks peaks ta hästi sobima, kusjuures price/performance suhe on Ruby ja Java-ga võrreldes ideaalilähedane. Hinna all mõtlen siin näiteks seda, et sellist väikest Scala skripti on kindlasti lihtsam ja mõnusam kirjutada kui vastavat Java koodi. Lisaks peaks muidugi veel märkima, et kui pole nii suurte failidega tegu, siis võib-olla pole jõudlus üldse oluline ja Ruby sobib täiesti (aga kindlasti tuleks REXML’i asemel kasutada mingit kiiremat teeki, nagu näiteks Hpricot). Kui on vaja eraldiseisvat skripti, mitte suurema süsteemi osa, siis Java’t igatahes ei kipu esialgu soovitama seda tüüpi ülesanneteks — järgmine kord vaatame kas äkki SAX muudab midagi. :)

Versioonid keeltest, mida kasutasin:

  • Ruby: 1.8.6
  • JRuby: 1.1
  • Scala: 2.7.0 (NB! selles versioonis on bugi mõningate XML failidega, mis 2.7.1 versioonis peaks korda saama)
  • Java: Sun JRE, 1.6.0

Veel mõned viited:

4 thoughts on “Ilus vs. kiire kood II: Ruby, Scala, Java”

  1. Päris vahva test. Ma omalt poolt lisaks testitavate keelte nimistusse Clojure, peaks Scalale igat pidi konkurentsi pakkuma.

    Aga selles suhtes on sul õigus, et see on pigem XPathi teekide test kui keelte võrdlemine. On üsna ilmne, et enamus aega veedetakse real, kus XPathi suurest dokumendist üksikut nodet otsitakse: doc.search(“beans/bean[@id='” + ref + “‘]”)

    Kui ma võrdluseks Ruby lahenduse libxml2 (native library) peale ümber kirjutasin, siis tuli tulemus kätte juba 8 sekundiga. Ning arvestades sellega et Scala ei pea nodesid DOMi mööda taga otsima, pole seegi võrdlus väga aus. Näiteks kui rubys loopida noded id järgi hashi, siis väheneb aeg kaunis talutava .45 sekundi peale. Karta on, et Scala puhul ongi noded mingis listist mõistlikumas andmetüübis, mis seletaks kiiruse erinevuse javaga võrreldes.

  2. Hashi kasutamisel läheb ka Scala ja Java kood tunduvalt kiiremaks. Aga kui Ruby ka nii palju kiiremaks läheb siis on muidugi päris vinge. Järgmises postituses arvatavasti testikski sellist algoritmi, mis loeb andmed mingisse hash’i või set’i.

  3. Jah, võibolla mitte niivõrd teekide kui keeltele omaste lahenduste võrdlus.

    Ja muidugi selge on see, et selle probleemi lahendamiseks pole DOM kõige õigem andmestruktuur ja XPathi kasutamine kõige õigem algoritm :)

  4. Kui õigesti aru sain siis jooksutasid sa rubyt windowsi keskonnas. Võiksid proovida ka linuxi või maci keskkonnas. Olen lugenud, et windowsi all näiteks RoR serveri käivitamine vahest võtab oma 10 sekundit kauem aega.

Leave a Reply

Your email address will not be published. Required fields are marked *