การเขียนโปรแกรมเชิงฟังก์ชัน - ความไม่เปลี่ยนรูปมีราคาแพงหรือไม่? [ปิด]


98

คำถามเป็นสองส่วน ประการแรกคือแนวความคิด ต่อไปจะดูคำถามเดียวกันอย่างเป็นรูปธรรมมากขึ้นใน Scala

  1. การใช้เฉพาะโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปในภาษาโปรแกรมทำให้การใช้อัลกอริทึม / ลอจิกบางอย่างมีราคาแพงกว่าในทางปฏิบัติหรือไม่? สิ่งนี้นำมาสู่ความจริงที่ว่าการไม่เปลี่ยนรูปเป็นหลักการสำคัญของภาษาที่ใช้งานได้อย่างหมดจด มีปัจจัยอื่น ๆ ที่ส่งผลกระทบนี้หรือไม่?
  2. ลองมาเป็นตัวอย่างที่เป็นรูปธรรมมากขึ้น โดยทั่วไปแล้วQuicksortได้รับการสอนและนำไปใช้โดยใช้การดำเนินการที่ไม่แน่นอนบนโครงสร้างข้อมูลในหน่วยความจำ เราจะนำสิ่งดังกล่าวไปใช้อย่างไรในรูปแบบการทำงานที่บริสุทธิ์โดยมีค่าใช้จ่ายในการคำนวณและพื้นที่จัดเก็บที่เทียบเคียงกับเวอร์ชันที่เปลี่ยนแปลง โดยเฉพาะใน Scala ฉันได้รวมเกณฑ์มาตรฐานคร่าวๆไว้ด้านล่าง

รายละเอียดเพิ่มเติม:

ฉันมาจากพื้นฐานการเขียนโปรแกรมที่จำเป็น (C ++, Java) ฉันได้สำรวจการเขียนโปรแกรมเชิงฟังก์ชันโดยเฉพาะ Scala

หลักการหลักบางประการของการเขียนโปรแกรมเชิงฟังก์ชัน:

  1. หน้าที่เป็นพลเมืองชั้นหนึ่ง
  2. ฟังก์ชั่นไม่ได้มีผลข้างเคียงและด้วยเหตุนี้วัตถุโครงสร้างข้อมูล / มีไม่เปลี่ยนรูป

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

ในกรณีที่เฉพาะเจาะจงฉันรู้สึกประหลาดใจเล็กน้อยกับข้อมูลโค้ดในบทช่วยสอน 6นี้ มี Quicksort เวอร์ชัน Java ตามด้วยการใช้งาน Scala ที่ดูเรียบร้อยเหมือนกัน

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

เวอร์ชัน Java

public class QuickSortJ {

    public static void sort(int[] xs) {
      sort(xs, 0, xs.length -1 );
    }

    static void sort(int[] xs, int l, int r) {
      if (r >= l) return;
      int pivot = xs[l];
      int a = l; int b = r;
      while (a <= b){
        while (xs[a] <= pivot) a++;
        while (xs[b] > pivot) b--;
        if (a < b) swap(xs, a, b);
      }
      sort(xs, l, b);
      sort(xs, a, r);
    }

    static void swap(int[] arr, int i, int j) {
      int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
    }
}

รุ่น Scala

object QuickSortS {

  def sort(xs: Array[Int]): Array[Int] =
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length / 2)
      Array.concat(
        sort(xs filter (pivot >)),
        xs filter (pivot ==),
        sort(xs filter (pivot <)))
    }
}

Scala Code เพื่อเปรียบเทียบการใช้งาน

import java.util.Date
import scala.testing.Benchmark

class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {

  val ints = new Array[Int](100000);

  override def prefix = name
  override def setUp = {
    val ran = new java.util.Random(5);
    for (i <- 0 to ints.length - 1)
      ints(i) = ran.nextInt();
  }
  override def run = sortfn(ints)
}

val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut   = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java   " )

benchImmut.main( Array("5"))
benchMut.main( Array("5"))

ผล

เวลาเป็นมิลลิวินาทีสำหรับการวิ่งติดต่อกันห้าครั้ง

Immutable/Functional/Scala    467    178    184    187    183
Mutable/Imperative/Java        51     14     12     12     12

10
มีราคาแพงเมื่อนำไปใช้อย่างไร้เดียงสาหรือด้วยวิธีการที่เติบโตขึ้นสำหรับภาษาที่จำเป็น คอมไพเลอร์อัจฉริยะ (เช่น GHC คอมไพเลอร์ Haskell - และ Haskell มีเฉพาะค่าที่ไม่เปลี่ยนรูปเท่านั้น ) สามารถใช้ประโยชน์จากความไม่เปลี่ยนรูปและประสิทธิภาพการทำงานที่สามารถแข่งขันกับโค้ดโดยใช้ความสามารถในการเปลี่ยนแปลงได้ ไม่จำเป็นต้องพูดว่าการใช้งาน Quicksort ที่ไร้เดียงสายังคงเป็นไปอย่างช้าๆเพราะใช้ในสิ่งที่มีค่าใช้จ่ายอื่น ๆ การเรียกซ้ำจำนวนมากและO(n)รายการ concat มันสั้นกว่ารุ่น pseudocode;)

3
บทความบล็อกที่ดีและเกี่ยวข้องอยู่ที่นี่: blogs.sun.com/jrose/entry/larval_objects_in_the_vm สนับสนุนเขาเพราะสิ่งนี้จะเป็นประโยชน์ต่อ Java และภาษา VM ที่ใช้งานได้อย่างมาก
andersoj

2
เธรด SO นี้มีการอภิปรายโดยละเอียดมากมายเกี่ยวกับประสิทธิภาพของการเขียนโปรแกรมเชิงฟังก์ชัน stackoverflow.com/questions/1990464/… . ตอบสิ่งที่ฉันอยากรู้มากมาย
smartnut007

5
สิ่งที่ไร้เดียงสาที่สุดนี่คือเกณฑ์มาตรฐานของคุณ คุณไม่สามารถเปรียบเทียบอะไรกับโค้ดแบบนั้นได้! คุณควรอ่านบทความเกี่ยวกับการทำ Benchmark บน JVM อย่างจริงจังก่อนที่จะหาข้อสรุปใด ๆ ... คุณรู้หรือไม่ว่า JVM อาจยังไม่ได้ JITted โค้ดของคุณก่อนที่จะเรียกใช้ คุณได้ตั้งค่าขนาดสูงสุดและขนาดสูงสุดของฮีปอย่างเหมาะสมหรือไม่ (เพื่อที่คุณจะไม่พิจารณาเวลาที่กระบวนการ JVM ขอหน่วยความจำเพิ่มเติม) คุณทราบวิธีการใดบ้างที่กำลังรวบรวมหรือคอมไพล์ใหม่ คุณรู้จัก GC หรือไม่? ผลลัพธ์ที่คุณได้รับจากรหัสนี้ไม่มีความหมายอะไรเลย!
Bruno Reis

2
@userunknown ไม่นั่นเป็นการเปิดเผย การเขียนโปรแกรมที่จำเป็น "เปลี่ยนสถานะด้วยคำสั่ง" ในขณะที่การเขียนโปรแกรมเชิงฟังก์ชัน "เป็นกระบวนทัศน์การเขียนโปรแกรมที่เปิดเผย" ซึ่ง "หลีกเลี่ยงการเปลี่ยนสถานะ" ( Wikipedia ) ใช่แล้วการใช้งานและความจำเป็นเป็นสองสิ่งที่แตกต่างกันอย่างสิ้นเชิงและรหัสที่คุณเขียนก็ไม่จำเป็น
Brian McCutchon

คำตอบ:


106

เนื่องจากมีความเข้าใจผิดบางอย่างบินอยู่ที่นี่ฉันจึงขอชี้แจงบางประเด็น

  • Quicksort "ในสถานที่" ไม่ได้อยู่ในสถานที่จริง ๆ (และ Quicksort ไม่ได้เป็นไปตามคำจำกัดความในสถานที่) ต้องใช้พื้นที่จัดเก็บเพิ่มเติมในรูปแบบของพื้นที่สแต็กสำหรับขั้นตอนเรียกซ้ำซึ่งอยู่ในลำดับO (log n ) ในกรณีที่ดีที่สุด แต่O ( n ) ในกรณีที่แย่ที่สุด

  • การใช้งานตัวแปร Quicksort ที่ทำงานบนอาร์เรย์ทำให้ไม่บรรลุวัตถุประสงค์ อาร์เรย์จะไม่เปลี่ยนรูป

  • การใช้ฟังก์ชัน Quicksort ที่ "เหมาะสม" ใช้รายการที่ไม่เปลี่ยนรูป แน่นอนว่ามันไม่ได้อยู่ในสถานที่ แต่ก็มีรันไทม์ asymptotic กรณีที่เลวร้ายที่สุด ( O ( n ^ 2)) และความซับซ้อนของพื้นที่ ( O ( n )) เป็นเวอร์ชันแทนขั้นตอน

    โดยเฉลี่ยแล้วเวลาในการทำงานของมันยังคงเท่ากันกับตัวแปรที่อยู่ในตำแหน่ง ( O ( n log n )) อย่างไรก็ตามความซับซ้อนของพื้นที่ยังคงเป็นO ( n )


มีข้อเสียที่ชัดเจนสองประการของการใช้งาน Quicksort ที่ใช้งานได้ ต่อไปนี้ลองพิจารณาการนำไปใช้อ้างอิงนี้ใน Haskell (ฉันไม่รู้จัก Scala ... ) จากบทนำของ Haskell :

qsort []     = []
qsort (x:xs) = qsort lesser ++ [x] ++ qsort greater
    where lesser  = (filter (< x) xs)
          greater = (filter (>= x) xs)
  1. ข้อเสียประการแรกคือการเลือกองค์ประกอบเดือยซึ่งไม่ยืดหยุ่นมาก จุดแข็งของการใช้งาน Quicksort ที่ทันสมัยขึ้นอยู่กับตัวเลือกที่ชาญฉลาดของเดือย (เปรียบเทียบ“ วิศวกรรมฟังก์ชันการจัดเรียง” โดย Bentley et al . อัลกอริทึมข้างต้นไม่ดีในเรื่องนั้นซึ่งทำให้ประสิทธิภาพโดยเฉลี่ยลดลงอย่างมาก

  2. ประการที่สองอัลกอริทึมนี้ใช้การต่อรายการ (แทนการสร้างรายการ) ซึ่งเป็นการดำเนินการO ( n ) สิ่งนี้ไม่ส่งผลกระทบต่อความซับซ้อนของ asymptotic แต่เป็นปัจจัยที่วัดได้

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


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

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

หลังจากอ่าน“ วิศวกรรมฟังก์ชันการจัดเรียง” ฉันไม่สามารถพิจารณาอัลกอริทึมที่สวยงามได้อีกต่อไป ใช้งานได้อย่างมีประสิทธิภาพมันเป็นงานที่ยุ่งเหยิงเป็นผลงานของวิศวกรไม่ใช่ของศิลปิน (ไม่ใช่เพื่อลดคุณค่าทางวิศวกรรม! สิ่งนี้มีความสวยงามในตัวเอง)


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

และความไม่เปลี่ยนรูปสามารถลดต้นทุนด้านประสิทธิภาพได้ด้วยการขจัดความจำเป็นในการทำสำเนาที่มีราคาแพงหรือการซิงโครไนซ์ข้ามเธรด

ดังนั้นเพื่อตอบคำถามเดิม“ ความไม่เปลี่ยนรูปมีราคาแพงหรือไม่? ” - ในกรณีเฉพาะของ Quicksort มีค่าใช้จ่ายที่แน่นอนเป็นผลมาจากการไม่เปลี่ยนรูป แต่โดยทั่วไปไม่มี


10
+1 - คำตอบที่ดี! ถึงแม้ว่าผมจะเองได้จบลงด้วยบางครั้งมากกว่าไม่มี ยังคงเป็นเพียงบุคลิกภาพ - คุณได้อธิบายปัญหาได้เป็นอย่างดี
Rex Kerr

6
คุณควรเพิ่มว่าการนำไปใช้งานที่เหมาะสมโดยใช้ค่าที่ไม่เปลี่ยนรูปนั้นสามารถใช้งานแบบขนานได้ทันทีเมื่อเทียบกับเวอร์ชันที่จำเป็น ในบริบททางเทคโนโลยีสมัยใหม่สิ่งนี้มีความสำคัญมากขึ้นเรื่อย ๆ
Raphael

จะใช้qsort lesser ++ (x : qsort greater)ความช่วยเหลือมากแค่ไหน?
Solomon Ucko

42

มีหลายสิ่งผิดปกติกับสิ่งนี้เป็นเกณฑ์มาตรฐานของการเขียนโปรแกรมเชิงฟังก์ชัน จุดเด่น ได้แก่ :

  • คุณกำลังใช้งานแบบดั้งเดิมซึ่งอาจต้องบรรจุกล่อง / ไม่มีกล่อง คุณไม่ได้พยายามทดสอบค่าใช้จ่ายในการห่อวัตถุดั้งเดิมคุณกำลังพยายามทดสอบความไม่เปลี่ยนรูป
  • คุณได้เลือกอัลกอริทึมที่การดำเนินการในสถานที่มีประสิทธิภาพผิดปกติ (และพิสูจน์ได้เช่นนั้น) หากคุณต้องการแสดงให้เห็นว่ามีอัลกอริทึมที่เร็วกว่าเมื่อนำมาใช้ร่วมกันนี่เป็นทางเลือกที่ดี มิฉะนั้นสิ่งนี้มีแนวโน้มที่จะทำให้เข้าใจผิด
  • คุณกำลังใช้ฟังก์ชันจับเวลาผิด ใช้System.nanoTime.
  • เกณฑ์มาตรฐานสั้นเกินไปที่คุณจะมั่นใจได้ว่าการรวบรวม JIT จะไม่เป็นส่วนสำคัญของเวลาที่วัดได้
  • อาร์เรย์ไม่ได้แบ่งอย่างมีประสิทธิภาพ
  • อาร์เรย์สามารถเปลี่ยนแปลงได้ดังนั้นการใช้กับ FP จึงเป็นการเปรียบเทียบที่แปลก

ดังนั้นการเปรียบเทียบนี้จึงเป็นภาพประกอบที่ดีที่คุณต้องเข้าใจรายละเอียดภาษา (และอัลกอริทึม) ของคุณเพื่อเขียนโค้ดที่มีประสิทธิภาพสูง แต่มันไม่ใช่การเปรียบเทียบระหว่าง FP กับไม่ใช่ FP ที่ดีนัก หากคุณต้องการที่ตรวจสอบHaskell เทียบกับ C ++ ที่เกณฑ์มาตรฐานเกมคอมพิวเตอร์ภาษา ข้อความรับกลับบ้านมีว่าโดยทั่วไปแล้วบทลงโทษจะไม่เกิน 2 หรือ 3 เท่า แต่มันขึ้นอยู่กับจริงๆ (ไม่มีคำสัญญาว่าชาว Haskell ได้เขียนอัลกอริทึมที่เร็วที่สุดเท่าที่จะเป็นไปได้ แต่อย่างน้อยก็มีบางคนที่พยายาม! จากนั้นอีกครั้ง Haskell บางคนเรียกไลบรารี C ... )

ตอนนี้สมมติว่าคุณต้องการเกณฑ์มาตรฐานของ Quicksort ที่สมเหตุสมผลมากขึ้นโดยตระหนักว่านี่อาจเป็นหนึ่งในกรณีที่เลวร้ายที่สุดสำหรับ FP กับอัลกอริทึมที่เปลี่ยนแปลงไม่ได้และไม่สนใจปัญหาโครงสร้างข้อมูล (เช่นแสร้งทำเป็นว่าเราสามารถมีอาร์เรย์ที่ไม่เปลี่ยนรูปได้):

object QSortExample {
  // Imperative mutable quicksort
  def swap(xs: Array[String])(a: Int, b: Int) {
    val t = xs(a); xs(a) = xs(b); xs(b) = t
  }
  def muQSort(xs: Array[String])(l: Int = 0, r: Int = xs.length-1) {
    val pivot = xs((l+r)/2)
    var a = l
    var b = r
    while (a <= b) {
      while (xs(a) < pivot) a += 1
      while (xs(b) > pivot) b -= 1
      if (a <= b) {
        swap(xs)(a,b)
        a += 1
        b -= 1
      }
    }
    if (l<b) muQSort(xs)(l, b)
    if (b<r) muQSort(xs)(a, r)
  }

  // Functional quicksort
  def fpSort(xs: Array[String]): Array[String] = {
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length/2)
      val (small,big) = xs.partition(_ < pivot)
      if (small.length == 0) {
        val (bigger,same) = big.partition(_ > pivot)
        same ++ fpSort(bigger)
      }
      else fpSort(small) ++ fpSort(big)
    }
  }

  // Utility function to repeat something n times
  def repeat[A](n: Int, f: => A): A = {
    if (n <= 1) f else { f; repeat(n-1,f) }
  }

  // This runs the benchmark
  def bench(n: Int, xs: Array[String], silent: Boolean = false) {
    // Utility to report how long something took
    def ptime[A](f: => A) = {
      val t0 = System.nanoTime
      val ans = f
      if (!silent) printf("elapsed: %.3f sec\n",(System.nanoTime-t0)*1e-9)
      ans
    }

    if (!silent) print("Scala builtin ")
    ptime { repeat(n, {
      val ys = xs.clone
      ys.sorted
    }) }
    if (!silent) print("Mutable ")
    ptime { repeat(n, {
      val ys = xs.clone
      muQSort(ys)()
      ys
    }) }
    if (!silent) print("Immutable ")
    ptime { repeat(n, {
      fpSort(xs)
    }) }
  }

  def main(args: Array[String]) {
    val letters = (1 to 500000).map(_ => scala.util.Random.nextPrintableChar)
    val unsorted = letters.grouped(5).map(_.mkString).toList.toArray

    repeat(3,bench(1,unsorted,silent=true))  // Warmup
    repeat(3,bench(10,unsorted))     // Actual benchmark
  }
}

สังเกตการปรับเปลี่ยน Quicksort ที่ใช้งานได้เพื่อให้ผ่านข้อมูลเพียงครั้งเดียวถ้าเป็นไปได้ทั้งหมดและเปรียบเทียบกับการเรียงลำดับในตัว เมื่อเราเรียกใช้เราจะได้รับสิ่งที่ต้องการ:

Scala builtin elapsed: 0.349 sec
Mutable elapsed: 0.445 sec
Immutable elapsed: 1.373 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.441 sec
Immutable elapsed: 1.374 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.442 sec
Immutable elapsed: 1.383 sec

ดังนั้นนอกเหนือจากการเรียนรู้ว่าการพยายามเขียนเรียงลำดับของคุณเองนั้นเป็นความคิดที่ไม่ดีเราพบว่ามีโทษ ~ 3 เท่าสำหรับการเรียงตัวเร็วที่ไม่เปลี่ยนรูปหากมีการใช้อย่างหลังอย่างระมัดระวัง (คุณยังสามารถเขียนเมธอด trisect ที่ส่งคืนอาร์เรย์สามอาร์เรย์ซึ่งน้อยกว่าค่าที่เท่ากันและมากกว่า pivot ซึ่งอาจทำให้เร็วขึ้นเล็กน้อย


เกี่ยวกับการชกมวย / แกะกล่อง หากมีสิ่งใดที่ควรเป็นโทษทางฝั่ง java ใช่ไหม? ไม่ Int เป็นประเภทตัวเลขที่ต้องการสำหรับ Scala (เทียบกับจำนวนเต็ม) ดังนั้นจึงไม่มีการชกมวยในฝั่งสกาล่า การชกมวยเป็นเพียงปัญหาในฝั่ง java เนื่องจากการทำ autoboxing จาก scala Int ไปยัง java.lang.Integer / int นี่คือลิงค์ที่พูดถึงเรื่องนี้โดยละเอียดansorg-it.com/th/scalanews-001.html
smartnut007

ใช่ฉันกำลังเล่นเป็นผู้สนับสนุนปีศาจที่นี่ ความสามารถในการเปลี่ยนแปลงได้เป็นส่วนหนึ่งของการออกแบบตัวดูด นั่นเป็นเหตุผลที่ฉันสงสัยมากเกี่ยวกับแนวทางการทำงานที่แท้จริงของปัญหา เฮ้อฉันพูดคำนี้เป็นครั้งที่ 10 แล้วในกระทู้ :-) จะดูส่วนที่เหลือของโพสต์ของคุณเมื่อฉันตื่นและกลับมา ขอบคุณ.
smartnut007

2
@ smartnut007 - คอลเลกชัน Scala เป็นของทั่วไป Generics ต้องการประเภทบรรจุกล่องเป็นส่วนใหญ่ (แม้ว่าจะมีความพยายามในการทำให้เชี่ยวชาญสำหรับประเภทดั้งเดิมบางประเภท) ดังนั้นคุณจึงไม่สามารถใช้วิธีการรวบรวมที่ดีทั้งหมดและคิดว่าจะไม่มีการลงโทษเมื่อคุณส่งคอลเลกชันของประเภทดั้งเดิมผ่านพวกเขา ค่อนข้างเป็นไปได้ว่าประเภทดั้งเดิมจะต้องถูกบรรจุระหว่างทางเข้าและแกะกล่องระหว่างทางออก
Rex Kerr

ฉันไม่ชอบความจริงที่ว่าข้อบกพร่องด้านบนที่คุณระบุเป็นเพียงการเก็งกำไร :-)
smartnut007

1
@ smartnut007 - เป็นข้อบกพร่องอันดับต้น ๆ เพราะตรวจสอบยากและถ้าจริงจะทำให้ผลลัพธ์เสียหาย หากคุณมั่นใจว่าไม่มีการชกมวยฉันยอมรับว่าข้อบกพร่องนั้นไม่ถูกต้อง ข้อบกพร่องไม่ได้อยู่ที่การชกมวย แต่คุณไม่รู้ว่ามีชกมวยหรือไม่ (และฉันก็ไม่แน่ใจเหมือนกัน - ความเชี่ยวชาญทำให้เข้าใจยาก) ในฝั่ง Java (หรือการปรับใช้ Scala ที่ไม่สามารถใช้งานได้) ไม่มีมวยเนื่องจากคุณใช้เฉพาะแบบดั้งเดิม อย่างไรก็ตามเวอร์ชันที่ไม่เปลี่ยนรูปจะทำงานผ่าน n log n space ดังนั้นคุณจึงต้องเปรียบเทียบค่าใช้จ่ายในการเปรียบเทียบ / swap กับการจัดสรรหน่วยความจำ
Rex Kerr

10

ฉันไม่คิดว่ารุ่น Scala เป็นจริง recursive Array.concatหางเนื่องจากคุณกำลังใช้

นอกจากนี้เนื่องจากเป็นรหัส Scala ที่เป็นสำนวนไม่ได้หมายความว่าเป็นวิธีที่ดีที่สุดที่จะทำ

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

ดูคำถาม Stack Overflow ฉันจะเรียงลำดับอาร์เรย์ใน Scala ได้อย่างไร สำหรับตัวอย่าง


4
นอกจากนี้ฉันไม่คิดว่าจะมีการเรียงลำดับอย่างรวดเร็วแบบหางซ้ำได้เนื่องจากคุณต้องโทรซ้ำสองครั้ง
Alex Lo

1
เป็นไปได้คุณต้องใช้การปิดแบบต่อเนื่องเพื่อยกเฟรมที่จะเป็นสแต็กของคุณเข้าสู่ฮีป
Brian

inbuilt scala.util.Sorting.quickSort (array) กลายพันธุ์อาร์เรย์ มันเร็วพอ ๆ กับ java ไม่แปลกใจเลย ฉันสนใจโซลูชันที่ใช้งานได้จริงที่มีประสิทธิภาพ ถ้าไม่ทำไม เป็นข้อ จำกัด ของ Scala หรือกระบวนทัศน์เชิงหน้าที่โดยทั่วไปหรือไม่ สิ่งนั้น
smartnut007

@ smartnut007: คุณใช้ Scala รุ่นอะไร? ใน Scala 2.8 คุณสามารถทำสิ่งarray.sortedที่ส่งคืนอาร์เรย์ที่เรียงลำดับใหม่โดยไม่เปลี่ยนรูปแบบเดิม
missingfaktor

@AlexLo - มีหางเรียงซ้ำอย่างรวดเร็วเป็นไปได้ สิ่งที่ชอบ:TAIL-RECURSIVE-QUICKSORT(Array A, int lo, int hi): while p < r: q = PARTITION(A, lo, hi); TAIL-RECURSIVE-QUICKSORT(A, lo, q - 1); p = q + 1;
Jakeway

8

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

พูดง่ายๆคือคุณไม่ต้องใช้ Quicksort เมื่อใช้ภาษาที่ใช้งานได้จริง

ลองพิจารณาจากอีกมุมหนึ่ง ลองพิจารณาสองฟังก์ชันนี้:

// Version using mutable data structures
def tailFrom[T : ClassManifest](arr: Array[T], p: T => Boolean): Array[T] = {
  def posIndex(i: Int): Int = {
    if (i < arr.length) {
      if (p(arr(i)))
        i
      else
        posIndex(i + 1)
    } else {
      -1
    }
  }

  var index = posIndex(0)

  if (index < 0) Array.empty
  else {
    var result = new Array[T](arr.length - index)
    Array.copy(arr, index, result, 0, arr.length - index)
    result
  }
}

// Immutable data structure:

def tailFrom[T](list: List[T], p: T => Boolean): List[T] = {
  def recurse(sublist: List[T]): List[T] = {
    if (sublist.isEmpty) sublist
    else if (p(sublist.head)) sublist
    else recurse(sublist.tail)
  }
  recurse(list)
}

เกณฑ์มาตรฐานนั้นและคุณจะพบว่าโค้ดที่ใช้โครงสร้างข้อมูลที่เปลี่ยนแปลงได้นั้นมีประสิทธิภาพที่แย่กว่ามากเพราะต้องคัดลอกอาร์เรย์ในขณะที่โค้ดที่ไม่เปลี่ยนรูปไม่จำเป็นต้องเกี่ยวข้อง

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

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


7

การจัดเรียงอาร์เรย์เป็นงานที่จำเป็นที่สุดในจักรวาล ไม่น่าแปลกใจที่กลยุทธ์ / การนำไปใช้ที่ 'ไม่เปลี่ยนรูป' ที่สง่างามจำนวนมากล้มเหลวในไมโครเบนช์มาร์ก 'เรียงลำดับอาร์เรย์' สิ่งนี้ไม่ได้หมายความว่าความไม่เปลี่ยนรูปมีราคาแพง "โดยทั่วไป" มีหลายงานที่การใช้งานที่ไม่เปลี่ยนรูปจะดำเนินการได้โดยเปรียบเทียบกับงานที่เปลี่ยนแปลงได้ แต่การเรียงลำดับอาร์เรย์มักจะไม่ใช่หนึ่งในนั้น


7

หากคุณเพียงแค่เขียนอัลกอริทึมและโครงสร้างข้อมูลที่จำเป็นของคุณใหม่เป็นภาษาที่ใช้งานได้จริงมันจะมีราคาแพงและไร้ประโยชน์ เพื่อให้สิ่งต่าง ๆ เปล่งประกายคุณควรใช้คุณสมบัติที่มีให้เฉพาะในการเขียนโปรแกรมเชิงฟังก์ชันเท่านั้น: ข้อมูลจะคงอยู่ถาวรการประเมินที่ขี้เกียจเป็นต้น


คุณใจดีพอที่จะให้การใช้งานใน Scala ได้ไหม
smartnut007

3
powells.com/biblio/17-0521631246-0 (โครงสร้างข้อมูลที่ใช้งานได้อย่างหมดจดโดย Chris Okasaki) - เพียงแค่อ่านหนังสือเล่มนี้ มีเรื่องราวที่ชัดเจนในการใช้ประโยชน์จากประโยชน์ของการเขียนโปรแกรมเชิงฟังก์ชันเมื่อใช้อัลกอริทึมและโครงสร้างข้อมูลที่มีประสิทธิภาพ
Vasil Remeniuk

1
code.google.com/p/pfdsโครงสร้างข้อมูลบางส่วนที่ใช้ใน Scala โดย Debashish Ghosh
Vasil Remeniuk

คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงคิดว่า Scala ไม่จำเป็น list.filter (foo).sort (bar).take (10)- อะไรจะมีความจำเป็นมากกว่านี้?
ไม่ทราบผู้ใช้

7

ค่าใช้จ่ายของการไม่เปลี่ยนรูปในScala

นี่คือเวอร์ชันที่เกือบจะเร็วกว่า Java ;)

object QuickSortS {
  def sort(xs: Array[Int]): Array[Int] = {
    val res = new Array[Int](xs.size)
    xs.copyToArray(res)
    (new QuickSortJ).sort(res)
    res
  }
}

เวอร์ชันนี้สร้างสำเนาของอาร์เรย์จัดเรียงโดยใช้เวอร์ชัน Java และส่งคืนสำเนา Scala ไม่ได้บังคับให้คุณใช้โครงสร้างที่ไม่เปลี่ยนรูปภายใน

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


แม้ว่านี่จะไม่ใช่คำตอบที่ชัดเจนสำหรับคำถาม แต่ฉันคิดว่ามันเป็นส่วนหนึ่งของคำตอบที่ดี: Quicksort เร็วกว่าเมื่อใช้โครงสร้างที่ไม่แน่นอน แต่ข้อดีหลัก ๆ ของความไม่เปลี่ยนรูปคืออินเทอร์เฟซและใน Scala อย่างน้อยคุณก็สามารถมีทั้งสองอย่างได้ ความสามารถในการกลายพันธุ์นั้นเร็วกว่าสำหรับ Quicksort แต่นั่นไม่ได้อยู่ในวิธีการเขียนของคุณโดยส่วนใหญ่เป็นโค้ดที่ไม่เปลี่ยนรูป
Paul Draper

7

QuickSort เป็นที่ทราบกันดีว่าเร็วกว่าเมื่อทำในสถานที่ดังนั้นจึงแทบจะไม่เป็นการเปรียบเทียบที่ยุติธรรม!

ต้องบอกว่า ... Array.concat? หากไม่มีอะไรอื่นคุณกำลังแสดงให้เห็นว่าประเภทคอลเลกชันที่ปรับให้เหมาะกับการเขียนโปรแกรมที่จำเป็นนั้นทำงานได้ช้าโดยเฉพาะเมื่อคุณลองใช้ในอัลกอริทึมการทำงาน ทางเลือกอื่น ๆ เกือบจะเร็วกว่า!


อีกประเด็นที่สำคัญมากที่ต้องพิจารณาซึ่งอาจเป็นประเด็นที่สำคัญที่สุดเมื่อเปรียบเทียบทั้งสองแนวทางคือ: "การขยายขนาดนี้ไปยังหลายโหนด / คอร์ได้ดีเพียงใด"

มีโอกาสที่ถ้าคุณกำลังมองหา Quicksort ที่ไม่เปลี่ยนรูปแสดงว่าคุณกำลังทำเช่นนั้นเพราะคุณต้องการ Quicksort แบบขนาน Wikipedia มีการอ้างอิงบางส่วนเกี่ยวกับเรื่องนี้: http://en.wikipedia.org/wiki/Quicksort#Parallelizations

เวอร์ชันสกาลาสามารถแยกก่อนที่ฟังก์ชันจะวนซ้ำทำให้สามารถจัดเรียงรายการที่มีหลายพันล้านรายการได้อย่างรวดเร็วหากคุณมีคอร์เพียงพอ

ตอนนี้ GPU ในระบบของฉันมี 128 คอร์สำหรับฉันถ้าฉันสามารถเรียกใช้โค้ด Scala ได้และนี่เป็นระบบเดสก์ท็อปธรรมดาที่อยู่เบื้องหลังรุ่นปัจจุบันสองปี

มันจะซ้อนทับกับวิธีการที่จำเป็นแบบเธรดเดียวได้อย่างไร ...

บางทีคำถามที่สำคัญกว่าคือ:

"เนื่องจากว่าแต่ละคอร์จะไม่เร็วขึ้นและการซิงโครไนซ์ / การล็อกเป็นความท้าทายที่แท้จริงสำหรับการขนานกันการเปลี่ยนแปลงนั้นมีราคาแพงหรือไม่"


ไม่มีข้อโต้แย้งที่นั่น Quicksort เป็นคำจำกัดความในการเรียงลำดับในหน่วยความจำ ฉันแน่ใจว่าคนส่วนใหญ่จำได้ตั้งแต่สมัยเรียนมหาวิทยาลัย แต่คุณจะ Quicksort ด้วยวิธีการทำงานที่แท้จริงได้อย่างไร คือไม่มีผลข้างเคียง
smartnut007

สาเหตุที่สำคัญมีการอ้างว่ากระบวนทัศน์ในการทำงานสามารถทำได้เร็วพอ ๆ กับฟังก์ชันที่มีผลข้างเคียง
smartnut007

เวอร์ชันรายการลดเวลาลงครึ่งหนึ่ง ยังไม่ใช่ที่ใดก็ตามที่ใกล้เคียงกับความเร็วของเวอร์ชันจาวา
smartnut007

คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงคิดว่า Scala ไม่จำเป็น list.filter (foo).sort (bar).take (10)- อะไรจะมีความจำเป็นมากกว่านี้? ขอบคุณ.
ไม่ทราบผู้ใช้

@ ไม่ทราบผู้ใช้: บางทีคุณอาจอธิบายได้ว่าคุณคิดว่า "จำเป็น" หมายถึงอะไรเพราะตัวอย่างที่คุณระบุไว้ดูเหมาะกับฉัน Scala เองก็ไม่มีความจำเป็นหรือเป็นคำประกาศภาษานี้รองรับทั้งสองรูปแบบและคำเหล่านี้ใช้อธิบายตัวอย่างที่เฉพาะเจาะจงได้ดีที่สุด
Kevin Wright

2

มีการกล่าวกันว่าการเขียนโปรแกรม OO ใช้นามธรรมเพื่อซ่อนความซับซ้อนและการเขียนโปรแกรมเชิงฟังก์ชันใช้ความไม่เปลี่ยนรูปเพื่อลบความซับซ้อน ในโลกไฮบริดของ Scala เราสามารถใช้ OO เพื่อซ่อนรหัสที่จำเป็นโดยไม่ต้องใช้รหัสแอปพลิเคชันที่ฉลาดกว่า อันที่จริงไลบรารีคอลเลกชันนั้นใช้โค้ดที่จำเป็นมากมาย แต่ไม่ได้หมายความว่าเราไม่ควรใช้มัน อย่างที่คนอื่น ๆ บอกใช้อย่างระมัดระวังคุณจะได้รับสิ่งที่ดีที่สุดจากทั้งสองโลก


คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงคิดว่า Scala ไม่จำเป็น list.filter (foo).sort (bar).take (10)- อะไรจะมีความจำเป็นมากกว่านี้? ขอบคุณ.
ไม่ทราบผู้ใช้

ฉันไม่เห็นตรงไหนที่เขาบอกว่าสกาล่าไม่จำเป็น
ม.ค. x
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.