Hiệu suất của Scala so với Java


41

Trước hết tôi muốn làm rõ rằng đây không phải là câu hỏi ngôn ngữ-X-ngôn ngữ-Y để xác định câu nào tốt hơn.

Tôi đã sử dụng Java trong một thời gian dài và tôi dự định tiếp tục sử dụng nó. Song song với điều này, tôi hiện đang học Scala rất quan tâm: ngoài những điều nhỏ nhặt làm quen với ấn tượng của tôi là tôi thực sự có thể làm việc rất tốt trong ngôn ngữ này.

Câu hỏi của tôi là: làm thế nào để phần mềm được viết bằng Scala so với phần mềm được viết bằng Java về tốc độ thực thi và mức tiêu thụ bộ nhớ? Tất nhiên, đây là một câu hỏi khó trả lời nói chung, nhưng tôi mong đợi rằng các cấu trúc cấp cao hơn như khớp mẫu, các hàm bậc cao hơn, v.v., giới thiệu một số chi phí.

Tuy nhiên, trải nghiệm hiện tại của tôi về Scala chỉ giới hạn ở các ví dụ nhỏ dưới 50 dòng mã và tôi chưa chạy bất kỳ điểm chuẩn nào cho đến nay. Vì vậy, tôi không có dữ liệu thực sự.

Nếu hóa ra Scala một số Java wrt trên đầu, thì có nghĩa là có các dự án Scala / Java hỗn hợp, trong đó một mã hóa các phần phức tạp hơn trong Scala và các phần quan trọng về hiệu năng trong Java? Đây có phải là một thực tế phổ biến?

CHỈNH SỬA 1

Tôi đã chạy một điểm chuẩn nhỏ: xây dựng một danh sách các số nguyên, nhân mỗi số nguyên với hai và đặt nó vào một danh sách mới, in danh sách kết quả. Tôi đã viết một triển khai Java (Java 6) và triển khai Scala (Scala 2.9). Tôi đã chạy cả trên Indigo Eclipse trong Ubuntu 10.04.

Các kết quả tương đương nhau: 480 ms cho Java và 493 ms cho Scala (trung bình hơn 100 lần lặp). Dưới đây là những đoạn tôi đã sử dụng.

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

Vì vậy, trong trường hợp này, dường như chi phí Scala (sử dụng phạm vi, bản đồ, lambda) thực sự rất nhỏ, không xa với thông tin được cung cấp bởi Kỹ sư Thế giới.

Có lẽ có các cấu trúc Scala khác nên được sử dụng cẩn thận vì chúng đặc biệt nặng để thực thi?

CHỈNH SỬA 2

Một số bạn chỉ ra rằng các println trong các vòng bên trong chiếm phần lớn thời gian thực hiện. Tôi đã xóa chúng và đặt kích thước của danh sách thành 100000 thay vì 20000. Trung bình kết quả là 88 ms cho Java và 49 ms cho Scala.


5
Tôi tưởng tượng rằng vì Scala biên dịch thành mã byte JVM, nên về mặt lý thuyết hiệu năng có thể tương đương với Java chạy dưới cùng một JVM, tất cả những thứ khác đều bằng nhau. Sự khác biệt tôi nghĩ là ở cách trình biên dịch Scala tạo mã byte và nếu nó hoạt động hiệu quả như vậy.
maple_shaft

2
@maple_shaft: Hoặc có thể có chi phí trong thời gian biên dịch Scala?
Thất vọngWithFormsDesigner

1
@Giorgio Không có sự phân biệt thời gian chạy giữa các đối tượng Scala và các đối tượng Java, chúng đều là các đối tượng JVM được định nghĩa và hành xử theo mã byte. Scala chẳng hạn như một ngôn ngữ có khái niệm về các bao đóng, nhưng khi chúng được biên dịch, chúng được biên dịch thành một số lớp với mã byte. Về mặt lý thuyết, tôi có thể viết mã Java về mặt vật lý có thể biên dịch thành mã byte chính xác và hành vi thời gian chạy sẽ hoàn toàn giống nhau.
maple_shaft

2
@maple_shaft: Đó chính xác là những gì tôi đang hướng tới: Tôi thấy mã Scala ở trên ngắn gọn và dễ đọc hơn nhiều so với mã Java tương ứng. Tôi chỉ tự hỏi liệu có nên viết các phần của dự án Scala bằng Java vì lý do hiệu năng hay không, và những phần đó sẽ là gì.
Giorgio

2
Thời gian chạy sẽ bị chiếm phần lớn bởi các cuộc gọi println. Bạn cần một bài kiểm tra chuyên sâu tính toán hơn.
kevin cline

Câu trả lời:


39

Có một điều mà bạn có thể thực hiện một cách chính xác và hiệu quả trong Java mà bạn không thể có trong Scala: liệt kê. Đối với mọi thứ khác, ngay cả đối với các cấu trúc chậm trong thư viện của Scala, bạn có thể có các phiên bản hiệu quả làm việc trong Scala.

Vì vậy, đối với hầu hết các phần, bạn không cần thêm Java vào mã của mình. Ngay cả đối với mã sử dụng phép liệt kê trong Java, thường có một giải pháp trong Scala là đầy đủ hoặc tốt - tôi đặt ngoại lệ cho các liệt kê có các phương thức bổ sung và sử dụng các giá trị không đổi int.

Đối với những gì cần chú ý, đây là một số điều.

  • Nếu bạn sử dụng mẫu thư viện phong phú của tôi, luôn chuyển đổi thành một lớp. Ví dụ:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
    
  • Hãy cảnh giác với các phương thức thu thập - vì phần lớn chúng là đa hình, JVM không tối ưu hóa chúng. Bạn không cần phải tránh chúng, nhưng hãy chú ý đến nó trên các phần quan trọng. Xin lưu ý rằng fortrong Scala được thực hiện thông qua các cuộc gọi phương thức và các lớp ẩn danh.

  • Nếu sử dụng một lớp Java, chẳng hạn như String, Arrayhoặc AnyValcác lớp tương ứng với các nguyên hàm Java, thì thích các phương thức do Java cung cấp khi có các lựa chọn thay thế. Ví dụ, sử dụng lengthtrên StringArraythay vì size.

  • Tránh sử dụng bất cẩn các chuyển đổi ngầm định, vì bạn có thể thấy mình sử dụng các chuyển đổi do nhầm lẫn thay vì theo thiết kế.

  • Mở rộng các lớp thay vì các đặc điểm. Ví dụ, nếu bạn đang mở rộng Function1, AbstractFunction1thay vào đó hãy gia hạn .

  • Sử dụng -optimisevà chuyên môn hóa để có được hầu hết Scala.

  • Hiểu những gì đang xảy ra: javaplà bạn của bạn, và một loạt cờ Scala cho thấy những gì đang diễn ra.

  • Thành ngữ Scala được thiết kế để cải thiện tính chính xác và làm cho mã ngắn gọn và dễ bảo trì hơn. Chúng không được thiết kế cho tốc độ, vì vậy nếu bạn cần sử dụng nullthay vì Optiontrong một con đường quan trọng, hãy làm như vậy! Có một lý do tại sao Scala là đa mô hình.

  • Hãy nhớ rằng thước đo thực sự của hiệu suất là chạy mã. Xem câu hỏi này để biết ví dụ về những gì có thể xảy ra nếu bạn bỏ qua quy tắc đó.


1
+1: Rất nhiều thông tin hữu ích, ngay cả về các chủ đề mà tôi vẫn phải tìm hiểu, nhưng thật hữu ích khi đọc một số gợi ý trước khi tôi xem xét chúng.
Giorgio

Tại sao cách tiếp cận đầu tiên sử dụng sự phản chiếu? Dù sao nó cũng tạo ra lớp ẩn danh, vậy tại sao không sử dụng nó thay vì phản chiếu?
Oleksandr.Bezhan

@ Oleksandr.Bezhan Lớp ẩn danh là một khái niệm Java, không phải là Scala. Nó tạo ra một sàng lọc loại. Một phương thức lớp ẩn danh không ghi đè lớp cơ sở của nó có thể được truy cập từ bên ngoài. Điều tương tự cũng không đúng với các sàng lọc kiểu của Scala, vì vậy cách duy nhất để có được phương pháp đó là thông qua sự phản chiếu.
Daniel C. Sobral

Điều này nghe có vẻ khá khủng khiếp. Đặc biệt: "Hãy cảnh giác với các phương thức thu thập - vì phần lớn chúng là đa hình, JVM không tối ưu hóa chúng. Bạn không cần tránh chúng, mà hãy chú ý đến nó trên các phần quan trọng."
matt

21

Theo Trò chơi Điểm chuẩn cho một lõi, hệ thống 32 bit, Scala có tốc độ trung bình nhanh hơn 80% so với Java. Hiệu năng gần như tương đương với máy tính Quad Core x64. Ngay cả việc sử dụng bộ nhớ và mật độ mã cũng rất giống nhau trong hầu hết các trường hợp. Tôi sẽ nói dựa trên những phân tích (khá không khoa học) này rằng bạn đúng khi khẳng định rằng Scala bổ sung một số chi phí cho Java. Nó dường như không thêm hàng tấn chi phí, vì vậy tôi nghi ngờ chẩn đoán các mặt hàng bậc cao hơn chiếm nhiều không gian / thời gian là chính xác nhất.


2
Cho câu trả lời này xin vui lòng chỉ cần sử dụng so sánh trực tiếp như trang Trợ giúp gợi ý ( shootout.alioth.debian.org/help.php#comparetwo )
igouy

18
  • Hiệu suất của Scala rất tốt nếu bạn chỉ viết mã giống Java / C trong Scala. Trình biên dịch sẽ sử dụng JVM nguyên thủy cho Int, Charvv khi nó có thể. Trong khi các vòng lặp chỉ hiệu quả trong Scala.
  • Hãy nhớ rằng các biểu thức lambda được biên dịch thành các thể hiện của các lớp con ẩn danh của các Functionlớp. Nếu bạn chuyển lambda đến map, lớp ẩn danh cần được khởi tạo (và một số người địa phương có thể cần phải được thông qua), và sau đó mỗi lần lặp lại có thêm chức năng gọi hàm (với một số tham số truyền) từ các applycuộc gọi.
  • Nhiều lớp giống như scala.util.Randomchỉ là các hàm bao quanh các lớp JRE tương đương. Các cuộc gọi chức năng bổ sung là hơi lãng phí.
  • Xem ra cho ẩn ý trong mã hiệu suất quan trọng. java.lang.Math.signum(x)là trực tiếp hơn nhiều x.signum(), mà chuyển đổi RichIntvà trở lại.
  • Lợi ích hiệu năng chính của Scala so với Java là chuyên môn hóa. Hãy nhớ rằng chuyên môn hóa được sử dụng một cách tiết kiệm trong mã thư viện.

5
  • a) Từ kiến ​​thức hạn chế của tôi, tôi phải nhận xét rằng mã trong phương thức chính tĩnh không thể được tối ưu hóa rất tốt. Bạn nên di chuyển mã quan trọng đến một vị trí khác.
  • b) Từ những quan sát dài, tôi khuyên bạn không nên thực hiện đầu ra nặng trong kiểm tra hiệu suất (ngoại trừ đó chính xác là những gì bạn muốn tối ưu hóa, nhưng ai nên đọc 2 triệu giá trị?). Bạn đang đo println, điều đó không thú vị lắm. Thay thế println bằng max:
(1 to 20000).toList.map (_ * 2).max

giảm thời gian từ 800 ms xuống 20 trên hệ thống của tôi.

  • c) Sự hiểu biết được biết là hơi chậm (trong khi chúng ta phải thừa nhận rằng nó đang trở nên tốt hơn mọi lúc). Sử dụng các hàm while hoặc tailrecursive thay thế. Không phải trong ví dụ này, nơi nó là vòng lặp bên ngoài. Sử dụng chú thích @ tailrec-annotation, để kiểm tra tính nhạy cảm.
  • d) So sánh với C / Trình biên dịch thất bại. Bạn không viết lại mã scala cho các kiến ​​trúc khác nhau chẳng hạn. Sự khác biệt quan trọng khác đối với các tình huống lịch sử là
    • Trình biên dịch JIT, tối ưu hóa nhanh chóng và có thể linh hoạt, tùy thuộc vào dữ liệu đầu vào
    • Tầm quan trọng của bộ nhớ cache
    • Tầm quan trọng gia tăng của việc gọi song song. Scala ngày nay có các giải pháp để làm việc mà không cần nhiều chi phí song song. Điều đó là không thể trong Java, ngoại trừ bạn làm nhiều việc hơn.

2
Tôi đã xóa println khỏi vòng lặp và thực sự mã Scala nhanh hơn mã Java.
Giorgio

Việc so sánh với C và Trình biên dịch có nghĩa theo nghĩa sau: một ngôn ngữ cấp cao hơn có sự trừu tượng mạnh hơn nhưng bạn có thể cần sử dụng ngôn ngữ cấp thấp hơn để thực hiện. Liệu song song này có coi Scala là ngôn ngữ cấp cao hơn và Java là ngôn ngữ cấp thấp hơn không? Có lẽ là không, vì Scala dường như cung cấp hiệu năng tương tự như Java.
Giorgio

Tôi sẽ không nghĩ nó sẽ quan trọng với Clojure hoặc Scala nhưng khi tôi thường chơi với jRuby và Jython, tôi có lẽ đã viết mã quan trọng hiệu năng hơn trong Java. Với hai người đó tôi đã thấy một sự chênh lệch đáng kể nhưng đó là những năm trước đây ... có thể tốt hơn.
Giàn khoan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.