Интерпретация эталонных тестов на C, Clojure, Python, Ruby, Scala и других

Отказ

Я знаю, что искусственные тесты являются злыми. Они могут показывать результаты только для очень узкой ситуации. Я не предполагаю, что один язык лучше другого из-за какой-то глупой скамьи. Однако мне интересно, почему результаты настолько разные. Пожалуйста, см. Мои вопросы внизу.

Математическое сравнительное описание

Бенчмарк - это простые математические вычисления, чтобы найти пары простых чисел, которые отличаются на 6 (так называемые сексуальные штрихи) Например. сексуальные простые числа ниже 100 будут: (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)

Таблица результатов

В таблице: время вычисления в секундах Запуск: все, кроме Factor, выполнялось в VirtualBox (нестабильный гостевой компьютер Debian amd64, хост для Windows 7 x64) Процессор: 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] - Я боюсь представить, сколько времени займет это

Списки кодов

С

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 opimized isPrime (та же идея, что и в оптимизации 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 оптимизирован 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")

Фактор

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 (ЗШ):

#!/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

Вопросы

  • Почему Scala работает так быстро? Это из-за статического ввода? Или он просто использует JVM очень эффективно?
  • Почему такая огромная разница между Ruby и Python? Я думал, что эти два не совсем разные. Может быть, мой код ошибочен. Пожалуйста, просветите меня! Спасибо. UPD Да, это была ошибка в моем коде. Python и Ruby 1.9 довольно равны.
  • Действительно впечатляющий скачок производительности между версиями Ruby.
  • Могу ли я оптимизировать код Clojure, добавив объявления типов? Это поможет?

Ответ 1

Грубые ответы:

  • Scala статическая типизация помогает здесь совсем немного - это означает, что он использует JVM довольно эффективно без особых усилий.
  • Я не совсем уверен в различии Ruby/Python, но я подозреваю, что (2...n).all? в функции is-prime?, скорее всего, будет хорошо оптимизирован в Ruby (EDIT: похоже, что это действительно так, см. Джулиан отвечает за более подробно...)
  • Ruby 1.9.3 намного лучше оптимизирован.
  • Clojure код, безусловно, может быть ускорен! В то время как Clojure является динамическим по умолчанию, вы можете использовать подсказки типов, примитивные математические данные и т.д., Чтобы приблизиться к Scala/чистой скорости Java во многих случаях, когда вам нужно.

Самая важная оптимизация в коде Clojure заключалась бы в использовании типизированных математических примитивов в is-prime?, например:

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

С этим улучшением я получаю Clojure завершение 10k за 0,635 секунд (т.е. второй самый быстрый в вашем списке, избивающий Scala)

PS обратите внимание, что у вас есть код печати внутри вашего теста в некоторых случаях - не очень хорошая идея, так как искажает результаты, особенно если использовать функцию типа print в первый раз вызывает инициализацию подсистем IO или что-то в этом роде!

Ответ 2

Я отвечу только на # 2, так как это единственное, что у меня есть что-то разумное, чтобы сказать, но для вашего кода на Python вы создаете промежуточный список в is_prime, тогда как вы используете .map в вашем all в Ruby, который просто повторяется.

Если вы измените свой is_prime на:

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

они находятся на уровне.

Я мог бы оптимизировать Python дальше, но мой Ruby недостаточно хорош, чтобы знать, когда я дал больше преимуществ (например, использование xrange заставляет Python побеждать на моей машине, но я не помню, если используемый вами диапазон Ruby создает весь диапазон в памяти или нет).

РЕДАКТИРОВАТЬ: Не слишком глупо, делая код Python похожим:

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

который не меняется намного больше, ставит его на 1,5 секунды для меня и, будучи лишним глупым, запускает его с PyPy, ставит его на 0,3 с для 10K и 21s для 100K.

Ответ 3

Вот быстрая версия Clojure, используя те же основные алгоритмы:

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

Он работает примерно на 20 раз быстрее, чем ваш оригинал на моей машине. И здесь версия, которая использует новую библиотеку редукторов в 1.5 (требует Java 7 или 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)))

Это работает примерно в 40 раз быстрее, чем ваш оригинал. На моей машине это 100k за 1,5 секунды.

Ответ 4

Вы можете сделать Scala намного быстрее, изменив метод isPrime на

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

Не совсем лаконично, но программа работает в 40% случаев!

Мы вырезаем лишние Range и анонимные объекты Function, компилятор Scala распознает хвостовую рекурсию и превращает ее в цикл while, который JVM может превратить в более или менее оптимальный машинный код, поэтому он не должен быть слишком далеко от версии C.

См. также: Как оптимизировать для-понятий и циклов в Scala?

Ответ 5

Вот моя версия scala как параллельная, так и не параллельная, просто для удовольствия: (В моем двухъядерном вычислении параллельная версия занимает 335 мс, а непараллельная версия занимает 655 мс).

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

EDIT: Согласно предложению Эмиля Х, я изменил свой код, чтобы избежать эффектов разогрева IO и jvm:

Результат показан в моем вычислении:

Список (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)
  }
}

Ответ 6

Не обращайте внимания на контрольные показатели; проблема заинтересовала меня, и я сделал несколько быстрых трюков. Это использует декоратор lru_cache, который запоминает функцию; поэтому, когда мы называем is_prime(i-6), мы в основном получаем эту первичную проверку бесплатно. Это изменение сокращает работу примерно вдвое. Кроме того, мы можем совершать вызовы range() через только нечетные числа, сокращая работу примерно в два раза.

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

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

Для этого требуется Python 3.2 или новее получить lru_cache, но может работать с более старым Python, если вы установите рецепт Python, который предоставляет lru_cache. Если вы используете Python 2.x, вы должны использовать xrange() вместо 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)))

Вышеизложенное заняло очень мало времени для редактирования. Я решил сделать это еще на один шаг и сделать тест на простые числа, только попробуйте простые делители и только до квадратного корня от тестируемого числа. То, как я это делал, работает только в том случае, если вы проверяете цифры в порядке, чтобы он мог накапливать все простые числа; но эта проблема уже проверяла номера в порядке, чтобы это было хорошо.

На моем ноутбуке (ничего особенного, процессор - 1,5 ГГц AMD Turion II "K625" ), эта версия произвела ответ на 100K менее чем за 8 секунд.

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

Вышеприведенный код довольно легко писать в Python, Ruby и т.д., но будет больнее в C.

Вы не можете сравнивать номера в этой версии с номерами других версий, не переписывая другие, чтобы использовать аналогичные трюки. Я ничего здесь не пытаюсь доказать; Я просто подумал, что проблема была забавной, и я хотел посмотреть, какие легкие улучшения производительности я мог бы почерпнуть.

Ответ 7

Не забудьте Фортран! (В основном шутит, но я бы ожидал, что сработает с C). Заявления с восклицательными знаками являются необязательными, но хорошими. (! является символом комментария в 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

Ответ 8

Я не мог устоять перед некоторыми из наиболее очевидных оптимизаций для версии C, в результате которой тест 100k теперь принимает 0,3 секунды на моей машине (в 5 раз быстрее, чем версия C в вопросе, оба скомпилированы с 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 );
}

Вот идентичная реализация в 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" );
    }
}

С Java 1.7.0_04 это выполняется почти так же быстро, как версия C. Клиентская или серверная VM не показывают большой разницы, за исключением того, что обучение JIT, по-видимому, помогает серверной VM немного (~ 3%), хотя она практически не влияет на клиентскую VM. Вывод в Java кажется медленнее, чем на C. Если в обеих версиях выход заменяется статическим счетчиком, версия Java работает немного быстрее, чем версия C.

Это мои времена для запуска 100k:

  • 319ms C скомпилировано с /Ox и выводится в > NIL:
  • 312ms C, скомпилированный с /Ox и статическим счетчиком
  • 324ms Java-клиент VM с выходом в > NIL:
  • 299ms виртуальная машина Java со статическим счетчиком

и 1M run (результаты 16386):

  • 24.95s C скомпилировано с /Ox и статическим счетчиком
  • 25.08s Java-клиент VM со статическим счетчиком
  • 24.86s Java-сервер VM со статическим счетчиком

Хотя это не отвечает на ваши вопросы, это показывает, что небольшие настройки могут иметь заметное влияние на производительность. Поэтому, чтобы иметь возможность действительно сравнивать языки, вы должны стараться избегать всех алгоритмических различий как можно больше.

Он также дает подсказку, почему Scala выглядит довольно быстро. Он работает на виртуальной машине Java и, таким образом, выигрывает от впечатляющей производительности.

Ответ 9

В Scala попробуйте использовать Tuple2 вместо List, он должен идти быстрее. Просто удалите слово "List", так как (x, y) является Tuple2.

Tuple2 специализируется на Int, Long и Double, что означает, что ему не нужно вставлять/распаковывать эти исходные типы данных. Источник Tuple2. Список не специализируется. Источник списка.

Ответ 10

Здесь код для версии 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)
        }
    }
}

Он работал так же быстро, как версия C.

Использование Asus u81a Intel Core 2 Duo T6500 2,1 ГГц, 2 МБ кэш-памяти второго уровня, частота FSB 800 МГц. Оперативная память 4 ГБ

Версия 100k: C: 2.723s Go: 2.743s

С 1000000 (1M вместо 100K): C: 3m35.458s Go: 3m36.259s

Но я думаю, что было бы справедливо использовать Go, встроенный в возможности многопоточности, и сравнить эту версию с обычной версией C (без многопоточности), просто потому, что почти легко сделать многопоточность с Go.

Обновление: я сделал параллельную версию, используя Goroutines в 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
}

Распараллеливаемая версия используется в среднем 2,743 секунды, в то же время, что и обычная версия.

Распараллеленная версия завершена за 1,706 секунды. Он использовал менее 1,5 МБ оперативной памяти.

Одна вещь: мой двухъядерный процессор kubuntu 64bit никогда не достигал пика в обоих ядрах. Похоже, что Go использовал только одно ядро. Исправлено с вызовом runtime.GOMAXPROCS(4)

Обновление: я выполнил параллелизированную версию до 1M номеров. Один из моих ядер процессора был на 100% все время, а другой не использовался вообще (нечетно). Это заняло целую минуту больше, чем C и обычные версии Go.: (С >

С 1000000 (1M вместо 100K):

C: 3m35.458s Go: 3m36.259s Go using goroutines: 3m27.137s 2m16.125s

Версия 100k:

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

Ответ 11

Просто для удовольствия, вот параллельная версия Ruby.

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

На моем 1.8GHz Core i5 MacBook Air результаты работы:

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

Похоже, JVM JIT дает рубину хорошее повышение производительности в случае по умолчанию, в то время как истинная многопоточность помогает JRuby выполнять на 50% быстрее в поточном корпусе. Что еще более интересно, так это то, что JRuby 1.7 улучшает рейтинг JRuby 1.6 здоровыми 17%!

Ответ 12

На основе x4u answer я написал версию scala, используя рекурсию, и я улучшил ее, только перейдя к sqrt вместо x/2 для функция первичной проверки. Я получаю ~ 250 мс за 100 КБ и ~ 600 мс за 1М. Я пошел вперед и пошел к 10M в ~ 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")

Я также вернулся и написал версию CoffeeScript (V8 JavaScript), которая получает ~ 15 мс для 100 тыс., 250 мс для 1М и 6 с для 10М, используя счетчик (игнорируя ввод-вывод). Если я включаю вывод, то он принимает ~ 150 мс для 100 кБ, 1 с для 1М и 12 с для 10М. Невозможно использовать хвостовую рекурсию здесь, к сожалению, поэтому мне пришлось преобразовать ее обратно в циклы.

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

Ответ 13

Ответ на ваш вопрос № 1 заключается в том, что Да, JVM невероятно быстр и дается статическая типизация.

JVM должен работать быстрее, чем C в конечном итоге, возможно, даже быстрее, чем обычный язык ассемблера. Конечно, вы всегда можете вручную оптимизировать сборку, чтобы бить что угодно, выполняя рутинное профилирование во время выполнения и создавая отдельную версию для каждого процессора, вы просто должны быть удивительно хорошими и узнаваемыми.

Причины для скорости Java:

JVM может анализировать ваш код во время его запуска и вручную оптимизировать его - например, если у вас есть метод, который можно было бы статически проанализировать во время компиляции, чтобы быть истинным, и JVM заметила, что вы часто вызывали его с теми же параметрами, он МОЖЕТ на самом деле полностью отказаться от вызова и просто ввести результаты из последнего вызова (я не уверен, что Java действительно делает это именно так, но он делает много всего, как это).

Из-за статической типизации JVM может много узнать о вашем коде во время компиляции, это позволяет заранее оптимизировать немного вещей. Он также позволяет компилятору оптимизировать каждый класс отдельно, не зная, как другой класс планирует его использовать. Также у Java нет произвольных указателей на расположение памяти, она ЗНАЕТ, какие значения в памяти могут и не могут быть изменены и могут быть оптимизированы соответственно.

Распределение кучи MUCH более эффективно, чем C, распределение кучи Java больше похоже на распределение стека C в скорости - но более универсальное. Много времени прошло в разных альтроитах, используемых здесь, это искусство - например, все объекты с коротким сроком службы (например, переменные стека С) распределяются по "известному" свободному местоположению (без поиска свободного места с достаточным пространством) и все они освобождаются вместе за один шаг (например, стек pop).

JVM может знать причуды о вашей архитектуре процессора и генерировать машинный код специально для данного CPU.

JVM может быстро ускорить ваш код после его отправки. Подобно тому, как перенос программы на новый процессор может ускорить ее, перенос ее в новую версию JVM также может дать вам огромные скорости, которые можно было бы использовать для процессоров, которые даже не существовали, когда вы первоначально скомпилировали свой код, что-то c физически не может делать без рекомендации.

Кстати, большая часть плохой репутации для скорости java исходит из длительного времени запуска для загрузки JVM (когда-нибудь кто-то построит JVM в ОС, и это уйдет!) и тот факт, что многие разработчики действительно плохо при написании кода GUI (особенно в потоковом режиме), который заставил Java GUI часто становиться невосприимчивым и глючным. Простые в использовании языки, такие как Java и VB, имеют свои недостатки, усиленные тем, что возможности среднего программиста имеют тенденцию быть ниже, чем более сложные языки.