Diễn giải một điểm chuẩn trong C, Clojure, Python, Ruby, Scala và những thứ khác [đã đóng]


91

Khước từ

Tôi biết rằng điểm chuẩn nhân tạo là xấu. Chúng chỉ có thể hiển thị kết quả cho các tình huống hẹp rất cụ thể. Tôi không cho rằng ngôn ngữ này tốt hơn ngôn ngữ kia vì một số thứ ngu ngốc. Tuy nhiên tôi tự hỏi tại sao kết quả lại khác nhau như vậy. Vui lòng xem câu hỏi của tôi ở phía dưới.

Mô tả điểm chuẩn toán học

Điểm chuẩn là các phép tính toán học đơn giản để tìm các cặp số nguyên tố khác nhau bằng 6 (gọi là số nguyên tố sexy ) Ví dụ: các số nguyên tố sexy dưới 100 sẽ là:(5 11) (7 13) (11 17) (13 19) (17 23) (23 29) (31 37) (37 43) (41 47) (47 53) (53 59) (61 67) (67 73) (73 79) (83 89) (97 103)

Bảng kết quả

Trong bảng: thời gian tính bằng giây Đang chạy: tất cả ngoại trừ Factor đang chạy trong VirtualBox (máy khách amd64 không ổn định Debian, máy chủ Windows 7 x64) CPU: AMD A4-3305M

  Sexy primes up to:        10k      20k      30k      100k               

  Bash                    58.00   200.00     [*1]      [*1]

  C                        0.20     0.65     1.42     15.00

  Clojure1.4               4.12     8.32    16.00    137.93

  Clojure1.4 (optimized)   0.95     1.82     2.30     16.00

  Factor                    n/a      n/a    15.00    180.00

  Python2.7                1.49     5.20    11.00       119     

  Ruby1.8                  5.10    18.32    40.48    377.00

  Ruby1.9.3                1.36     5.73    10.48    106.00

  Scala2.9.2               0.93     1.41     2.73     20.84

  Scala2.9.2 (optimized)   0.32     0.79     1.46     12.01

[* 1] - Tôi sợ tưởng tượng sẽ mất bao nhiêu thời gian

Danh sách mã

C:

int isprime(int x) {
  int i;
  for (i = 2; i < x; ++i)
    if (x%i == 0) return 0;
  return 1;
}

void findprimes(int m) {
  int i;
  for ( i = 11; i < m; ++i)
    if (isprime(i) && isprime(i-6))
      printf("%d %d\n", i-6, i);
}

main() {
    findprimes(10*1000);
}

Ruby:

def is_prime?(n)
  (2...n).all?{|m| n%m != 0 }
end

def sexy_primes(x)
  (9..x).map do |i|
    [i-6, i]
  end.select do |j|
    j.all?{|j| is_prime? j}
  end
end

a = Time.now
p sexy_primes(10*1000)
b = Time.now
puts "#{(b-a)*1000} mils"

Scala:

def isPrime(n: Int) =
  (2 until n) forall { n % _ != 0 }

def sexyPrimes(n: Int) = 
  (11 to n) map { i => List(i-6, i) } filter { _ forall(isPrime(_)) }

val a = System.currentTimeMillis()
println(sexyPrimes(100*1000))
val b = System.currentTimeMillis()
println((b-a).toString + " mils")

Scala đã isPrimetối ưu hóa (ý tưởng tương tự như trong tối ưu hóa Clojure):

import scala.annotation.tailrec

@tailrec // Not required, but will warn if optimization doesn't work
def isPrime(n: Int, i: Int = 2): Boolean = 
  if (i == n) true 
  else if (n % i != 0) isPrime(n, i + 1)
  else false

Clojure:

(defn is-prime? [n]
  (every? #(> (mod n %) 0)
    (range 2 n)))

(defn sexy-primes [m]
  (for [x (range 11 (inc m))
        :let [z (list (- x 6) x)]
        :when (every? #(is-prime? %) z)]
      z))

(let [a (System/currentTimeMillis)]
  (println (sexy-primes (* 10 1000)))
  (let [b (System/currentTimeMillis)]
    (println (- b a) "mils")))

Clojure được tối ưu hóa is-prime?:

(defn ^:static is-prime? [^long n]
  (loop [i (long 2)] 
    (if (= (rem n i) 0)
      false
      (if (>= (inc i) n) true (recur (inc i))))))

Python

import time as time_

def is_prime(n):
  return all((n%j > 0) for j in xrange(2, n))

def primes_below(x):
  return [[j-6, j] for j in xrange(9, x+1) if is_prime(j) and is_prime(j-6)]

a = int(round(time_.time() * 1000))
print(primes_below(10*1000))
b = int(round(time_.time() * 1000))
print(str((b-a)) + " mils")

Hệ số

MEMO:: prime? ( n -- ? )
n 1 - 2 [a,b] [ n swap mod 0 > ] all? ;

MEMO: sexyprimes ( n n -- r r )
[a,b] [ prime? ] filter [ 6 + ] map [ prime? ] filter dup [ 6 - ] map ;

5 10 1000 * sexyprimes . .

Bash (zsh):

#!/usr/bin/zsh
function prime {
  for (( i = 2; i < $1; i++ )); do
    if [[ $[$1%i] == 0 ]]; then
      echo 1
      exit
    fi
  done
  echo 0
}

function sexy-primes {
  for (( i = 9; i <= $1; i++ )); do
    j=$[i-6]
    if [[ $(prime $i) == 0 && $(prime $j) == 0 ]]; then
      echo $j $i
    fi
  done
}

sexy-primes 10000

Câu hỏi

  1. Tại sao Scala lại nhanh như vậy? Có phải vì gõ tĩnh không? Hay nó chỉ đang sử dụng JVM rất hiệu quả?
  2. Tại sao lại có sự khác biệt lớn giữa Ruby và Python? Tôi nghĩ hai cái này không hoàn toàn khác nhau. Có lẽ mã của tôi là sai. Vui lòng làm sáng tỏ cho tôi! Cảm ơn. UPD Có, đó là lỗi trong mã của tôi. Python và Ruby 1.9 khá ngang nhau.
  3. Sự nhảy vọt thực sự ấn tượng về năng suất giữa các phiên bản Ruby.
  4. Tôi có thể tối ưu hóa mã Clojure bằng cách thêm khai báo kiểu không? Nó sẽ giúp?

6
@mgilson thực sự lên đến sqrt(n)nhưng có thể mất một chút thời gian để tính toán. Ngoài ra, mã C của bạn in ra các số nguyên tố khi nó tìm thấy chúng, trong khi các ngôn ngữ khác của bạn tính toán chúng thành danh sách và sau đó in chúng ra. Mặc dù C nhanh nhất không có gì đáng ngạc nhiên, nhưng bạn có thể có được nó nhanh hơn.
Russ

2
(Và dĩ nhiên là Sieve of Eratosthenes .. nhưng benchmark vi này là khá nhiều bài kiểm tra căng thẳng lặp và các hoạt động toán học Tuy nhiên, họ vẫn không phải là "công bằng" như trong một số là lười biếng hơn..)

2
Tôi vừa chạy cả phiên bản Go của tôi và phiên bản C của bạn (trông rất giống nhau) và tôi thực tế có tốc độ như nhau ở cả hai. Tôi chỉ thử phiên bản 100k : C: 2.723s Go: 2.743s.
Sebastián Grignoli

3
Bạn không cần phải tính toán sqrtcho việc kiểm tra này. Bạn có thể tính toán vuông inhư trongfor (i = 2; i * i <= x; ++i) ...
ivant

3
Tôi khuyên bạn nên chú thích Scala được tối ưu hóa isPrimevới @tailrec, để đảm bảo bạn đang sử dụng đệ quy đuôi. Rất dễ làm nhầm điều gì đó ngăn chặn đệ quy đuôi và chú thích này sẽ cảnh báo bạn nếu điều đó xảy ra.
Daniel C. Sobral

Câu trả lời:


30

Câu trả lời thô sơ:

  1. Tính năng nhập tĩnh của Scala đang giúp nó khá nhiều ở đây - điều này có nghĩa là nó sử dụng JVM khá hiệu quả mà không cần nỗ lực thêm.
  2. Tôi không chắc chắn chính xác về sự khác biệt của Ruby / Python, nhưng tôi nghi ngờ rằng (2...n).all?trong hàm is-prime?có khả năng được tối ưu hóa khá tốt trong Ruby (EDIT: nghe có vẻ như thực sự là vậy, hãy xem câu trả lời của Julian để biết thêm chi tiết ...)
  3. Ruby 1.9.3 được tối ưu hóa tốt hơn nhiều
  4. Mã Clojure chắc chắn có thể được tăng tốc rất nhiều! Mặc dù Clojure là động theo mặc định, bạn có thể sử dụng gợi ý kiểu, phép toán nguyên thủy, v.v. để đạt được tốc độ Scala / thuần Java trong nhiều trường hợp khi bạn cần.

Tối ưu hóa quan trọng nhất trong mã Clojure sẽ là sử dụng các phép toán nguyên thủy được đánh máy bên trong is-prime?, giống như:

(set! *unchecked-math* true) ;; at top of file to avoid using BigIntegers

(defn ^:static is-prime? [^long n]
  (loop [i (long 2)] 
    (if (zero? (mod n i))
      false
      (if (>= (inc i) n) true (recur (inc i))))))

Với cải tiến này, tôi nhận được Clojure hoàn thành 10k trong 0,635 giây (tức là nhanh thứ hai trong danh sách của bạn, đánh bại Scala)

Tái bút lưu ý rằng bạn có mã in bên trong điểm chuẩn của mình trong một số trường hợp - không phải là ý kiến ​​hay vì nó sẽ làm sai lệch kết quả, đặc biệt nếu việc sử dụng một chức năng như printlần đầu tiên gây ra việc khởi chạy hệ thống con IO hoặc tương tự!


2
Tôi không nghĩ là chút thông tin về Ruby và Python là nhất thiết phải đúng, nhưng 1 cách khác ..

Việc nhập không cho thấy bất kỳ kết quả ổn định có thể đo lường nào, nhưng tính năng mới của bạn is-prime?cho thấy sự cải thiện gấp đôi. ;)
defhlt

Điều này không thể được thực hiện nhanh hơn nếu có một mod không được kiểm tra?
Hendekagon

1
@Hendekagon - có lẽ! không chắc điều này được tối ưu hóa tốt như thế nào bởi trình biên dịch Clojure hiện tại, có lẽ vẫn còn chỗ để cải thiện. Clojure 1.4 chắc chắn giúp ích rất nhiều cho loại công cụ này, 1.5 có thể sẽ tốt hơn.
mikera

1
(zero? (mod n i))nên nhanh hơn(= (mod n i) 0)
Jonas

23

Đây là phiên bản Clojure nhanh, sử dụng cùng các thuật toán cơ bản:

(set! *unchecked-math* true)

(defn is-prime? [^long n]
  (loop [i 2]
    (if (zero? (unchecked-remainder-int n i))
      false
      (if (>= (inc i) n)
        true
        (recur (inc i))))))

(defn sexy-primes [m]
  (for [x (range 11 (inc m))
        :when (and (is-prime? x) (is-prime? (- x 6)))]
    [(- x 6) x]))

Nó chạy nhanh hơn khoảng 20 lần so với bản gốc của bạn trên máy của tôi. Và đây là phiên bản sử dụng thư viện bộ giảm tốc mới trong 1.5 (yêu cầu Java 7 hoặc JSR 166):

(require '[clojure.core.reducers :as r]) ;'

(defn sexy-primes [m]
  (->> (vec (range 11 (inc m)))
       (r/filter #(and (is-prime? %) (is-prime? (- % 6))))
       (r/map #(list (- % 6) %))
       (r/fold (fn ([] []) ([a b] (into a b))) conj)))

Điều này chạy nhanh hơn khoảng 40 lần so với bản gốc của bạn. Trên máy của tôi, đó là 100k trong 1,5 giây.


2
Sử dụng unchecked-remainder-inthoặc chỉ remthay vì modcùng với kết quả nhập tĩnh để tăng hiệu suất gấp 4 lần. Đẹp!
defhlt

22

Tôi sẽ trả lời chỉ # 2, vì nó là người duy nhất tôi đã có bất cứ điều gì từ xa thông minh để nói, nhưng đối với mã Python của bạn, bạn đang tạo ra một danh sách trung gian trong is_prime, trong khi bạn đang sử dụng .maptrong của bạnall trong Ruby mà chỉ là lặp đi lặp lại.

Nếu bạn thay đổi is_primethành:

def is_prime(n):
    return all((n%j > 0) for j in range(2, n))

họ ngang hàng.

Tôi có thể tối ưu hóa Python hơn nữa, nhưng Ruby của tôi không đủ tốt để biết khi nào tôi có nhiều lợi thế hơn (ví dụ: việc sử dụng xrangekhiến Python giành chiến thắng trên máy của tôi, nhưng tôi không nhớ liệu phạm vi Ruby bạn đã sử dụng có tạo ra không toàn bộ phạm vi trong bộ nhớ hoặc không).

CHỈNH SỬA: Không quá ngớ ngẩn, làm cho mã Python trông giống như:

import time

def is_prime(n):
    return all(n % j for j in xrange(2, n))

def primes_below(x):
    return [(j-6, j) for j in xrange(9, x + 1) if is_prime(j) and is_prime(j-6)]

a = int(round(time.time() * 1000))
print(primes_below(10*1000))
b = int(round(time.time() * 1000))
print(str((b-a)) + " mils")

điều này không thay đổi nhiều, đặt nó ở mức 1,5 giây đối với tôi và, với việc chạy nó với PyPy, nó ở mức 0,3 giây cho 10K và 21 giây cho 100K.


1
Trình tạo tạo ra sự khác biệt lớn ở đây vì nó cho phép chức năng bảo lãnh ngay lần đầu tiên False(bắt tốt).
mgilson

Tôi thực sự mong đợi họ sẽ tham gia PyPy ... Điều đó sẽ rất tuyệt vời.
mgilson

Bạn có vui lòng chạy câu trả lời của tôi trong PyPy không? Tôi tò mò rằng điều đó sẽ nhanh hơn bao nhiêu.
steveha

1
Bạn hoàn toàn đúng về cả điều lặp lại và xrange! Tôi đã sửa và bây giờ Python và Ruby hiển thị kết quả ngang nhau.
defhlt

1
@steveha Tôi sẽ chỉ làm như vậy nếu bạn hứa bây giờ hãy ra ngoài và tự tải xuống PyPy :)! pypy.org/download.html có mã nhị phân cho tất cả các hệ điều hành phổ biến và trình quản lý gói của bạn chắc chắn có nó. Dù sao, đối với điểm chuẩn của bạn, với một lru_cachetriển khai ngẫu nhiên cho 2,7 được tìm thấy trên AS, 100K chạy trong 2,3 giây.
Julian

16

Bạn có thể làm cho Scala nhanh hơn rất nhiều bằng cách sửa đổi isPrimephương pháp của bạn thành

  def isPrime(n: Int, i: Int = 2): Boolean = 
    if (i == n) true 
    else if (n % i != 0) isPrime(n, i + 1)
    else false

Không hoàn toàn ngắn gọn nhưng chương trình chạy trong 40% thời gian!

Chúng tôi cắt bỏ những thứ thừa Rangevà vô danhFunction đối tượng , trình biên dịch Scala nhận dạng đệ quy đuôi và biến nó thành một vòng lặp trong khi, JVM có thể biến thành mã máy tối ưu hơn hoặc ít hơn, vì vậy nó không nên quá xa C phiên bản.

Xem thêm: Làm thế nào để tối ưu hóa cho phần hiểu và vòng lặp trong Scala?


2
Cải tiến gấp đôi. Và liên kết tốt đẹp!
defhlt

btw thân phương pháp này là giống hệt nhau i == n || n % i != 0 && isPrime(n, i + 1), đó là ngắn hơn, mặc dù một chút khó khăn hơn để đọc
Luigi Plinge

1
Bạn nên thêm @tailrecchú thích, để đảm bảo nó sẽ thực hiện tối ưu hóa đó.
Daniel C. Sobral,

8

Đây là phiên bản scala của tôi ở cả song song và không song song, chỉ cho vui: (Trong máy tính lõi kép của tôi, phiên bản song song mất 335ms trong khi phiên bản không song song mất 655ms)

object SexyPrimes {
  def isPrime(n: Int): Boolean = 
    (2 to math.sqrt(n).toInt).forall{ n%_ != 0 }

  def isSexyPrime(n: Int): Boolean = isPrime(n) && isPrime(n-6)

  def findPrimesPar(n: Int) {
    for(k <- (11 to n).par)
      if(isSexyPrime(k)) printf("%d %d\n",k-6,k)
  }

  def findPrimes(n: Int) {
    for(k <- 11 to n)
      if(isSexyPrime(k)) printf("%d %d\n",k-6,k)
  }


  def timeOf(call : =>Unit) {
    val start = System.currentTimeMillis
    call
    val end = System.currentTimeMillis
    println((end-start)+" mils")
  }

  def main(args: Array[String]) {
    timeOf(findPrimes(100*1000))
    println("------------------------")
    timeOf(findPrimesPar(100*1000))
  }
}

CHỈNH SỬA: Theo gợi ý của Emil H , tôi đã thay đổi mã của mình để tránh ảnh hưởng của IO và khởi động jvm:

Kết quả hiển thị trong máy tính của tôi:

Danh sách (3432, 1934, 3261, 1716, 3229, 1654, 3214, 1700)

object SexyPrimes {
  def isPrime(n: Int): Boolean = 
    (2 to math.sqrt(n).toInt).forall{ n%_ != 0 }

  def isSexyPrime(n: Int): Boolean = isPrime(n) && isPrime(n-6)

  def findPrimesPar(n: Int) {
    for(k <- (11 to n).par)
      if(isSexyPrime(k)) ()//printf("%d %d\n",k-6,k)
  }

  def findPrimes(n: Int) {
    for(k <- 11 to n)
      if(isSexyPrime(k)) ()//printf("%d %d\n",k-6,k)
  }


  def timeOf(call : =>Unit): Long = {
    val start = System.currentTimeMillis
    call
    val end = System.currentTimeMillis
    end - start 
  }

  def main(args: Array[String]) {
    val xs = timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::
             timeOf(findPrimes(1000*1000))::timeOf(findPrimesPar(1000*1000))::Nil
    println(xs)
  }
}

1
Mã có bị ảnh hưởng bởi khởi động jvm không? Ví dụ như isSexyPrimecó thể có (thêm) tối ưu hóa khi gọi từ findPrimesParvà không quá nhiều khi gọi từfindPrimes
Emil H

@EmilH Đủ công bằng. Tôi đã thay đổi mã của mình để tránh ảnh hưởng của việc khởi động io và jvm.
Eastsun

Chỉ lên đến sqrt (n) là tối ưu hóa tốt, nhưng bây giờ bạn đang đo điểm chuẩn cho một thuật toán khác.
Luigi Plinge

7

Đừng bận tâm đến các điểm chuẩn; vấn đề khiến tôi quan tâm và tôi đã thực hiện một số chỉnh sửa nhanh. Điều này sử dụng trình lru_cachetrang trí, ghi nhớ một hàm; vì vậy khi chúng tôi gọi is_prime(i-6)về cơ bản chúng tôi sẽ nhận được séc chính miễn phí. Thay đổi này cắt giảm gần một nửa công việc. Ngoài ra, chúng ta có thể thực hiện các range()cuộc gọi chỉ qua các số lẻ, cắt giảm khoảng một nửa công việc.

http://en.wikipedia.org/wiki/Memoization

http://docs.python.org/dev/library/functools.html

Điều này yêu cầu Python 3.2 hoặc mới hơn để có được lru_cache, nhưng có thể hoạt động với Python cũ hơn nếu bạn cài đặt công thức Python cung cấp lru_cache. Nếu bạn đang sử dụng Python 2.x, bạn thực sự nên sử dụng xrange()thay vì range().

http://code.activestate.com/recipes/577479-simple-caching-decorator/

from functools import lru_cache
import time as time_

@lru_cache()
def is_prime(n):
    return n%2 and all(n%i for i in range(3, n, 2))

def primes_below(x):
    return [(i-6, i) for i in range(9, x+1, 2) if is_prime(i) and is_prime(i-6)]

correct100 = [(5, 11), (7, 13), (11, 17), (13, 19), (17, 23), (23, 29),
        (31, 37), (37, 43), (41, 47), (47, 53), (53, 59), (61, 67), (67, 73),
        (73, 79), (83, 89)]
assert(primes_below(100) == correct100)

a = time_.time()
print(primes_below(30*1000))
b = time_.time()

elapsed = b - a
print("{} msec".format(round(elapsed * 1000)))

Ở trên chỉ mất một thời gian rất ngắn để chỉnh sửa. Tôi quyết định tiến thêm một bước nữa và thực hiện kiểm tra số nguyên tố chỉ thử các ước số nguyên tố và chỉ tính đến căn bậc hai của số đang được kiểm tra. Cách tôi đã làm chỉ hoạt động nếu bạn kiểm tra các số theo thứ tự, vì vậy nó có thể tích lũy tất cả các số nguyên tố khi nó đi; nhưng vấn đề này đã được kiểm tra các con số theo thứ tự tốt.

Trên máy tính xách tay của tôi (không có gì đặc biệt; bộ xử lý là AMD Turion II "K625" 1,5 GHz), phiên bản này tạo ra câu trả lời cho 100K trong vòng chưa đầy 8 giây.

from functools import lru_cache
import math
import time as time_

known_primes = set([2, 3, 5, 7])

@lru_cache(maxsize=128)
def is_prime(n):
    last = math.ceil(math.sqrt(n))
    flag = n%2 and all(n%x for x in known_primes if x <= last)
    if flag:
        known_primes.add(n)
    return flag

def primes_below(x):
    return [(i-6, i) for i in range(9, x+1, 2) if is_prime(i) and is_prime(i-6)]

correct100 = [(5, 11), (7, 13), (11, 17), (13, 19), (17, 23), (23, 29),
        (31, 37), (37, 43), (41, 47), (47, 53), (53, 59), (61, 67), (67, 73),
        (73, 79), (83, 89)]
assert(primes_below(100) == correct100)

a = time_.time()
print(primes_below(100*1000))
b = time_.time()

elapsed = b - a
print("{} msec".format(round(elapsed * 1000)))

Đoạn mã trên khá dễ viết bằng Python, Ruby, v.v. nhưng sẽ khó hơn trong C.

Bạn không thể so sánh các số trên phiên bản này với các số từ các phiên bản khác mà không viết lại các số khác để sử dụng các thủ thuật tương tự. Tôi không cố gắng chứng minh bất cứ điều gì ở đây; Tôi chỉ nghĩ rằng vấn đề là vui và tôi muốn xem loại cải tiến hiệu suất dễ dàng mà tôi có thể thu thập được.


lru_cachechắc chắn là tiện lợi. Đối với một số loại vấn đề nhất định, chẳng hạn như tạo các số Fibonacci liên tiếp, nó có thể tăng tốc độ rất lớn chỉ bằng cách thêm trình trang trí một dòng đó vào hàm! Dưới đây là một liên kết đến một cuộc nói chuyện Raymond hettinger rằng bìa lru_cachekhoảng 26 phút trong. Blip.tv/pycon-us-videos-2009-2010-2011/...
steveha

3
Bằng cách sử dụng lru_cache, bạn thực sự sử dụng một thuật toán khác thay vì mã thô. Vì vậy, hiệu suất là về thuật toán chứ không phải ngôn ngữ của chính nó.
Eastsun

1
@Eastsun - Tôi không hiểu ý bạn. lru_cachetránh lặp lại một phép tính đã được thực hiện gần đây, và đó là tất cả; Tôi không hiểu đó là "thực sự là chúng tôi [sử dụng] một thuật toán khác". Và Python bị chậm, nhưng lợi ích từ việc có những thứ hay ho như lru_cache; Tôi không thấy có gì sai khi sử dụng các phần hữu ích của một ngôn ngữ. Và tôi đã nói rằng không nên so sánh thời gian chạy câu trả lời của tôi với các ngôn ngữ khác mà không thực hiện các thay đổi tương tự với các ngôn ngữ khác. Vì vậy, tôi không hiểu ý bạn.
steveha

@Eastsun đúng, nhưng mặt khác, nên cho phép sự thuận tiện của ngôn ngữ cấp cao hơn trừ khi đưa ra các ràng buộc bổ sung. lru_cache sẽ hy sinh bộ nhớ cho tốc độ và điều chỉnh độ phức tạp của thuật toán.
Matt Joiner

2
nếu bạn sử dụng một thuật toán khác, bạn có thể thử Sieve of Eratosthenes. Phiên bản Python đã đưa ra câu trả lời cho 100 nghìn đô la0.03 giây ( 30mili giây) .
jfs 27/07/12

7

Đừng quên Fortran! (Chủ yếu là nói đùa, nhưng tôi mong đợi hiệu suất tương tự như C). Các câu với dấu chấm than là tùy chọn, nhưng có phong cách tốt. ( !là một nhân vật bình luận trong fortran 90)

logical function isprime(n)
IMPLICIT NONE !
integer :: n,i
do i=2,n
   if(mod(n,i).eq.0)) return .false.
enddo
return .true.
end

subroutine findprimes(m)
IMPLICIT NONE !
integer :: m,i
logical, external :: isprime

do i=11,m
   if(isprime(i) .and. isprime(i-6))then
      write(*,*) i-6,i
   endif
enddo
end

program main
findprimes(10*1000)
end

6

Tôi không thể cưỡng lại việc thực hiện một số tối ưu hóa rõ ràng nhất cho phiên bản C, khiến bài kiểm tra 100k hiện mất 0,3 giây trên máy của tôi (nhanh hơn 5 lần so với phiên bản C trong câu hỏi, cả hai đều được biên dịch bằng MSVC 2010 / Ox) .

int isprime( int x )
{
    int i, n;
    for( i = 3, n = x >> 1; i <= n; i += 2 )
        if( x % i == 0 )
            return 0;
    return 1;
}

void findprimes( int m )
{
    int i, s = 3; // s is bitmask of primes in last 3 odd numbers
    for( i = 11; i < m; i += 2, s >>= 1 ) {
        if( isprime( i ) ) {
            if( s & 1 )
                printf( "%d %d\n", i - 6, i );
            s |= 1 << 3;
        }
    }
}

main() {
    findprimes( 10 * 1000 );
}

Đây là triển khai giống hệt nhau trong Java:

public class prime
{
    private static boolean isprime( final int x )
    {
        for( int i = 3, n = x >> 1; i <= n; i += 2 )
            if( x % i == 0 )
                return false;
        return true;
    }

    private static void findprimes( final int m )
    {
        int s = 3; // s is bitmask of primes in last 3 odd numbers
        for( int i = 11; i < m; i += 2, s >>= 1 ) {
            if( isprime( i ) ) {
                if( ( s & 1 ) != 0 )
                    print( i );
                s |= 1 << 3;
            }
        }
    }

    private static void print( int i )
    {
        System.out.println( ( i - 6 ) + " " + i );
    }

    public static void main( String[] args )
    {
        // findprimes( 300 * 1000 ); // for some JIT training
        long time = System.nanoTime();
        findprimes( 10 * 1000 );
        time = System.nanoTime() - time;
        System.err.println( "time: " + ( time / 10000 ) / 100.0 + "ms" );
    }
}

Với Java 1.7.0_04, điều này chạy gần như chính xác nhanh như phiên bản C. Máy khách hoặc máy chủ không có nhiều khác biệt, ngoại trừ việc đào tạo JIT dường như giúp máy chủ một chút (~ 3%) trong khi nó hầu như không có tác dụng với máy khách. Đầu ra trong Java có vẻ chậm hơn trong C. Nếu đầu ra được thay thế bằng bộ đếm tĩnh trong cả hai phiên bản, phiên bản Java chạy nhanh hơn một chút so với phiên bản C.

Đây là lần tôi chạy 100k:

  • 319ms C được biên dịch với / Ox và xuất ra> NIL:
  • 312ms C được biên dịch với / Ox và bộ đếm tĩnh
  • Máy ảo máy khách Java 324ms với đầu ra> NIL:
  • Máy ảo ứng dụng khách Java 299ms với bộ đếm tĩnh

và 1M lần chạy (16386 kết quả):

  • 24,95s C được biên dịch với / Ox và bộ đếm tĩnh
  • Máy ảo khách Java 25.08s với bộ đếm tĩnh
  • Máy chủ Java 24,86s với bộ đếm tĩnh

Mặc dù điều này không thực sự trả lời câu hỏi của bạn, nhưng nó cho thấy rằng các chỉnh sửa nhỏ có thể có tác động đáng kể đến hiệu suất. Vì vậy, để có thể thực sự so sánh các ngôn ngữ, bạn nên cố gắng tránh mọi khác biệt về thuật toán càng nhiều càng tốt.

Nó cũng cho biết lý do tại sao Scala có vẻ khá nhanh. Nó chạy trên máy ảo Java và do đó được hưởng lợi từ hiệu suất ấn tượng của nó.


1
Nhanh hơn để chuyển đến sqrt (x) thay vì x >> 1 cho hàm kiểm tra số nguyên tố.
Eve Freeman

4

Trong Scala, hãy thử sử dụng Tuple2 thay vì List, nó sẽ nhanh hơn. Chỉ cần loại bỏ từ 'Danh sách' vì (x, y) là một Tuple2.

Tuple2 chuyên dùng cho Int, Long và Double có nghĩa là nó sẽ không phải đóng / mở hộp các kiểu dữ liệu thô đó. Nguồn Tuple2 . Danh sách không chuyên biệt. Nguồn danh sách .


Sau đó, bạn không thể gọi foralltrên nó. Tôi cũng nghĩ rằng đây có thể không phải là mã hiệu quả nhất (nhiều hơn vì một bộ sưu tập nghiêm ngặt lớn được tạo ra cho quy mô lớn nthay vì chỉ sử dụng một chế độ xem), nhưng nó chắc chắn ngắn gọn + trang nhã và tôi đã rất ngạc nhiên về cách nó hoạt động tốt mặc dù sử dụng nhiều phong cách chức năng.
0__

Bạn nói đúng, tôi nghĩ 'forAll' đã ở đó. Vẫn phải có một cải tiến lớn so với List và sẽ không tệ lắm nếu có 2 cuộc gọi đó.
Tomas Lazaro

2
nó thực sự nhanh hơn, def sexyPrimes(n: Int) = (11 to n).map(i => (i-6, i)).filter({ case (i, j) => isPrime(i) && isPrime(j) })ở đây nhanh hơn khoảng 60%, vì vậy nên đánh bại mã C :)
0__

Hmm, tôi chỉ nhận được một sự gia tăng hiệu suất của 4 hoặc 5%
Luigi Plinge

1
Tôi thấy collectchậm hơn đáng kể. Nhanh hơn là nếu bạn thực hiện bộ lọc trước và sau đó lập bản đồ. withFilternhanh hơn một chút vì nó không thực sự tạo các bộ sưu tập trung gian. (11 to n) withFilter (i => isPrime(i - 6) && isPrime(i)) map (i => (i - 6, i))
Luigi Plinge,

4

Đây là mã cho phiên bản Go (golang.org):

package main

import (
    "fmt"
)


func main(){
    findprimes(10*1000)
}

func isprime(x int) bool {
    for i := 2; i < x; i++ {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func findprimes(m int){
    for i := 11; i < m; i++ {
        if isprime(i) && isprime(i-6) {
            fmt.Printf("%d %d\n", i-6, i)
        }
    }
}

Nó chạy nhanh như phiên bản C.

Sử dụng Asus u81a Intel Core 2 Duo T6500 2.1GHz, 2MB L2 cache, 800MHz FSB. RAM 4GB

Phiên bản 100k: C: 2.723s Go: 2.743s

Với 1000000 (1M thay vì 100K): C: 3m35.458s Go: 3m36.259s

Nhưng tôi nghĩ rằng sẽ là công bằng nếu sử dụng khả năng đa luồng tích hợp của Go và so sánh phiên bản đó với phiên bản C thông thường (không có đa luồng), chỉ vì quá dễ dàng để thực hiện đa luồng với Go.

Cập nhật: Tôi đã tạo một phiên bản song song bằng cách sử dụng Goroutines trong Go:

package main

import (
  "fmt"
  "runtime"
)

func main(){
    runtime.GOMAXPROCS(4)
    printer := make(chan string)
    printer2 := make(chan string)
    printer3 := make(chan string)
    printer4 := make(chan string)
    finished := make(chan int)

    var buffer, buffer2, buffer3 string

    running := 4
    go findprimes(11, 30000, printer, finished)
    go findprimes(30001, 60000, printer2, finished)
    go findprimes(60001, 85000, printer3, finished)
    go findprimes(85001, 100000, printer4, finished)

    for {
      select {
        case i := <-printer:
          // batch of sexy primes received from printer channel 1, print them
          fmt.Printf(i)
        case i := <-printer2:
          // sexy prime list received from channel, store it
          buffer = i
        case i := <-printer3:
          // sexy prime list received from channel, store it
          buffer2 = i
        case i := <-printer4:
          // sexy prime list received from channel, store it
          buffer3 = i
        case <-finished:
          running--
          if running == 0 {
              // all goroutines ended
              // dump buffer to stdout
              fmt.Printf(buffer)
              fmt.Printf(buffer2)
              fmt.Printf(buffer3)
              return
          }
      }
    }
}

func isprime(x int) bool {
    for i := 2; i < x; i++ {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func findprimes(from int, to int, printer chan string, finished chan int){
    str := ""
    for i := from; i <= to; i++ {
        if isprime(i) && isprime(i-6) {
            str = str + fmt.Sprintf("%d %d\n", i-6, i)
      }
    }
    printer <- str
    //fmt.Printf("Finished %d to %d\n", from, to)
    finished <- 1
}

Phiên bản song song được sử dụng trong trung bình 2,743 giây, cùng thời gian với phiên bản thông thường.

Phiên bản song song hoàn thành trong 1,706 giây. Nó sử dụng ít hơn 1,5 Mb RAM.

Một điều kỳ lạ: Kubuntu 64bit lõi kép của tôi không bao giờ đạt đỉnh ở cả hai lõi. Có vẻ như cờ vây chỉ sử dụng một lõi. Đã sửa lỗi bằng cuộc gọi tớiruntime.GOMAXPROCS(4)

Cập nhật: Tôi đã chạy phiên bản paralellized lên tới 1 triệu số. Một trong các lõi CPU của tôi luôn ở mức 100%, trong khi lõi còn lại hoàn toàn không được sử dụng (kỳ lạ). Nó mất hơn một phút so với phiên bản C và Go thông thường. :(

Với 1000000 (1M thay vì 100K):

C: 3m35.458s Go: 3m36.259s Go using goroutines:3m27.137 giây2m16.125s

Phiên bản 100k:

C: 2.723s Go: 2.743s Go using goroutines: 1.706s


Bạn đã sử dụng bao nhiêu lõi btw?
om-nom-nom

2
Tôi có một Asus u81a Intel Core 2 Duo T6500 2.1GHz, 2MB L2 cache, 800MHz FSB. RAM 4GB
Sebastián Grignoli

Bạn đã thực sự biên dịch phiên bản C với tính năng tối ưu hóa được bật chưa? Trình biên dịch Go mặc định không nội dòng và thường sẽ bị ảnh hưởng hiệu suất lớn so với C được tối ưu hóa trong các loại so sánh này. Thêm -O3hoặc tốt hơn.
Matt Joiner

Tôi chỉ làm, không phải trước, và phiên bản 100K mất cùng một lượng thời gian có hoặc không có O3
Sebastián Grignoli

Điều tương tự đối với phiên bản 1M. Có thể các hoạt động cụ thể này (chúng tôi đang thử nghiệm một tập hợp con rất nhỏ) được tối ưu hóa tốt theo mặc định.
Sebastián Grignoli,

4

Để cho vui, đây là một phiên bản Ruby song song.

require 'benchmark'

num = ARGV[0].to_i

def is_prime?(n)
  (2...n).all?{|m| n%m != 0 }
end

def sexy_primes_default(x)
    (9..x).map do |i|
        [i-6, i]
    end.select do |j|
        j.all?{|j| is_prime? j}
    end
end

def sexy_primes_threads(x)
    partition = (9..x).map do |i|
        [i-6, i]
    end.group_by do |x|
        x[0].to_s[-1]
    end
    threads = Array.new
    partition.each_key do |k|
       threads << Thread.new do
            partition[k].select do |j|
                j.all?{|j| is_prime? j}
            end
        end
    end
    threads.each {|t| t.join}
    threads.map{|t| t.value}.reject{|x| x.empty?}
end

puts "Running up to num #{num}"

Benchmark.bm(10) do |x|
    x.report("default") {a = sexy_primes_default(num)}
    x.report("threads") {a = sexy_primes_threads(num)}
end

Trên MacBook Air Core i5 1.8GHz của tôi, kết quả hiệu suất là:

# Ruby 1.9.3
$ ./sexyprimes.rb 100000
Running up to num 100000
                 user     system      total        real
default     68.840000   0.060000  68.900000 ( 68.922703)
threads     71.730000   0.090000  71.820000 ( 71.847346)

# JRuby 1.6.7.2 on JVM 1.7.0_05
$ jruby --1.9 --server sexyprimes.rb 100000
Running up to num 100000
                user     system      total        real
default    56.709000   0.000000  56.709000 ( 56.708000)
threads    36.396000   0.000000  36.396000 ( 36.396000)

# JRuby 1.7.0.preview1 on JVM 1.7.0_05
$ jruby --server sexyprimes.rb 100000
Running up to num 100000
             user     system      total        real
default     52.640000   0.270000  52.910000 ( 51.393000)
threads    105.700000   0.290000 105.990000 ( 30.298000)

Có vẻ như JIT của JVM đang mang lại cho Ruby một hiệu suất tốt hơn trong trường hợp mặc định, trong khi đa luồng thực sự giúp JRuby hoạt động nhanh hơn 50% trong trường hợp phân luồng. Điều thú vị hơn là JRuby 1.7 cải thiện điểm số JRuby 1.6 lên 17%!


3

Dựa trên câu trả lời của x4u , tôi đã viết một phiên bản scala bằng cách sử dụng đệ quy và tôi đã cải thiện nó bằng cách chỉ chuyển đến sqrt thay vì x / 2 cho hàm kiểm tra số nguyên tố. Tôi nhận được ~ 250ms với 100k và ~ 600ms với 1M. Tôi đã tiếp tục và đi đến 10 triệu trong ~ 6s.

import scala.annotation.tailrec

var count = 0;
def print(i:Int) = {
  println((i - 6) + " " + i)
  count += 1
}

@tailrec def isPrime(n:Int, i:Int = 3):Boolean = {
  if(n % i == 0) return false;
  else if(i * i > n) return true;
  else isPrime(n = n, i = i + 2)
}      

@tailrec def findPrimes(max:Int, bitMask:Int = 3, i:Int = 11):Unit = {
  if (isPrime(i)) {
    if((bitMask & 1) != 0) print(i)
    if(i + 2 < max) findPrimes(max = max, bitMask = (bitMask | (1 << 3)) >> 1, i = i + 2)
  } else if(i + 2 < max) {
    findPrimes(max = max, bitMask = bitMask >> 1, i = i + 2)
  }
}

val a = System.currentTimeMillis()
findPrimes(max=10000000)
println(count)
val b = System.currentTimeMillis()
println((b - a).toString + " mils")

Tôi cũng đã quay lại và viết một phiên bản CoffeeScript (V8 JavaScript), nhận được ~ 15ms với 100k, 250ms với 1M và 6s với 10M, bằng cách sử dụng bộ đếm (bỏ qua I / O). Nếu tôi bật đầu ra, nó mất ~ 150ms cho 100k, 1s cho 1M và 12s cho 10M. Rất tiếc, không thể sử dụng đệ quy đuôi ở đây, vì vậy tôi phải chuyển đổi nó trở lại thành các vòng lặp.

count = 0;
print = (i) ->
  console.log("#{i - 6} #{i}")
  count += 1
  return

isPrime = (n) ->
  i = 3
  while i * i < n
    if n % i == 0
      return false
    i += 2
  return true

findPrimes = (max) ->
  bitMask = 3
  for i in [11..max] by 2
    prime = isPrime(i)
    if prime
      if (bitMask & 1) != 0
        print(i)
      bitMask |= (1 << 3)
    bitMask >>= 1
  return

a = new Date()
findPrimes(1000000)
console.log(count)
b = new Date()
console.log((b - a) + " ms")

2

Câu trả lời cho câu hỏi số 1 của bạn là Có, JVM cực kỳ nhanh và có tính năng nhập tĩnh giúp ích.

JVM phải nhanh hơn C về lâu dài, thậm chí có thể nhanh hơn cả hợp ngữ "Thông thường" - Tất nhiên bạn luôn có thể tối ưu hóa thủ công lắp ráp để đánh bại mọi thứ bằng cách thực hiện cấu hình thời gian chạy thủ công và tạo một phiên bản riêng cho mỗi CPU, bạn chỉ cần phải trở nên tốt và có kiến ​​thức đáng kinh ngạc.

Lý do cho tốc độ của Java là:

JVM có thể phân tích mã của bạn trong khi chạy và tối ưu hóa bằng tay - ví dụ: nếu bạn có một phương thức có thể được phân tích tĩnh tại thời điểm biên dịch để trở thành một hàm thực và JVM nhận thấy rằng bạn thường gọi nó bằng tham số, nó sẽ thực sự loại bỏ hoàn toàn cuộc gọi và chỉ đưa kết quả từ lần gọi cuối cùng (tôi không chắc liệu Java có thực sự làm điều này chính xác hay không, nhưng nó làm được rất nhiều thứ như thế này).

Do tính năng nhập tĩnh, JVM có thể biết rất nhiều về mã của bạn tại thời điểm biên dịch, điều này cho phép nó tối ưu hóa trước khá nhiều thứ. Nó cũng cho phép trình biên dịch tối ưu hóa từng lớp riêng lẻ mà không cần biết về cách lớp khác dự định sử dụng nó. Ngoài ra, Java không có con trỏ tùy ý đến vị trí bộ nhớ, nó BIẾT những giá trị nào trong bộ nhớ có thể và không thể thay đổi và có thể tối ưu hóa cho phù hợp.

Phân bổ đống hiệu quả hơn C NHIỀU, phân bổ đống của Java giống như phân bổ ngăn xếp của C về tốc độ - nhưng linh hoạt hơn. Rất nhiều thời gian đã đi vào các thuật ngữ khác nhau được sử dụng ở đây, đó là một nghệ thuật - ví dụ: tất cả các đối tượng có tuổi thọ ngắn (như các biến ngăn xếp của C) được phân bổ đến một vị trí miễn phí "đã biết" (không tìm kiếm vị trí miễn phí với đủ không gian) và tất cả được giải phóng cùng nhau trong một bước duy nhất (giống như cửa sổ ngăn xếp).

JVM có thể biết những điều kỳ quặc về kiến ​​trúc CPU của bạn và tạo mã máy cụ thể cho một CPU nhất định.

JVM có thể tăng tốc mã của bạn sau khi bạn gửi mã. Giống như việc chuyển một chương trình sang một CPU mới có thể tăng tốc nó, chuyển nó sang phiên bản mới của JVM cũng có thể mang lại cho bạn hiệu suất tốc độ khổng lồ được truyền tay vào các CPU thậm chí không tồn tại khi bạn biên dịch mã ban đầu, điều mà về mặt vật lý không thể làm mà không có một dự đoán.

Nhân tiện, hầu hết các đại diện xấu cho tốc độ java đến từ thời gian khởi động lâu để tải JVM (Một ngày nào đó ai đó sẽ xây dựng JVM vào hệ điều hành và điều này sẽ biến mất!) Và thực tế là nhiều nhà phát triển thực sự rất tệ trong việc viết Mã GUI (đặc biệt là theo luồng) khiến các GUI của Java thường không phản hồi và trục trặc. Các ngôn ngữ đơn giản để sử dụng như Java và VB có lỗi của chúng được khuếch đại bởi thực tế là khả năng của các lập trình viên trung bình có xu hướng thấp hơn các ngôn ngữ phức tạp hơn.


Nói việc phân bổ heap của JVM hiệu quả hơn nhiều so với C là không hợp lý, vì JVM được viết bằng C ++.
Daniel C. Sobral

5
@ DanielC.Sobral ngôn ngữ không quan trọng bằng impelemntation - mã thực thi "Heap" của Java không giống như C. Java's là một hệ thống đa giai đoạn có thể thay thế, có thể tùy chỉnh cao cho các mục tiêu khác nhau với nhiều năm nỗ lực nghiên cứu bao gồm các kỹ thuật tiên tiến đang được phát triển ngày nay, C sử dụng heap - Một cấu trúc dữ liệu đơn giản được phát triển từ nhiều năm trước. Hệ thống của Java không thể triển khai cho C vì C cho phép con trỏ nên nó không bao giờ có thể đảm bảo di chuyển "An toàn" đối với các phần bộ nhớ được cấp phát tùy ý mà không có thay đổi ngôn ngữ (hiển thị nó không còn là C)
Bill K

An toàn là không liên quan - bạn không khẳng định nó an toàn hơn , bạn đã khẳng định nó hiệu quả hơn . Hơn nữa, bạn mô tả trong nhận xét về cách hoạt động của C "heap" không liên quan đến thực tế.
Daniel C. Sobral,

Chắc hẳn bạn đã hiểu sai ý nghĩa của tôi về "An toàn" - Java có thể di chuyển khối bộ nhớ tùy ý bất cứ lúc nào vì nó biết nó có thể, C không thể chọn tùy chọn bộ nhớ vì có thể có một con trỏ có thể tham chiếu đến nó. Ngoài ra AC heap thường được triển khai dưới dạng heap là một cấu trúc dữ liệu. C ++ heap đã từng được triển khai với các cấu trúc heap như C (Do đó có tên là "Heap") Tôi đã không đăng ký vào C ++ trong một vài năm nên điều này có thể không còn đúng nữa, nhưng nó vẫn bị hạn chế bởi không thể sắp xếp lại các phần nhỏ bộ nhớ được cấp phát của người dùng theo ý muốn.
Bill K
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.