Інтерпретація еталону в C, Clojure, Python, Ruby, Scala та інших [закрито]


91

Застереження

Я знаю, що штучні орієнтири - це зло. Вони можуть показати результати лише для дуже конкретної вузької ситуації. Я не припускаю, що одна мова краща за іншу через якусь дурну лавку. Однак мені цікаво, чому результати настільки різні. Будь ласка, перегляньте мої запитання внизу.

Опис математичного тесту

Бенчмарк - це прості математичні обчислення для пошуку пар простих чисел, які відрізняються на 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] - Я боюся уявити, скільки часу це займе

Списки кодів

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

Рубін:

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 оптимізована 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 . .

Баш (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

Питання

  1. Чому Scala так швидка? Це через статичне введення тексту ? Або це просто використання JVM дуже ефективно?
  2. Чому така величезна різниця між Ruby та Python? Я думав, що ці два не зовсім різняться. Можливо, мій код помилковий. Просвіти мене, будь ласка! Дякую. UPD Так, це була помилка в моєму коді. Python та Ruby 1.9 досить рівні.
  3. Дійсно вражаючий стрибок продуктивності між версіями Ruby.
  4. Чи можу я оптимізувати код Clojure, додаючи декларації типу? Це допоможе?

6
@mgilson насправді до, sqrt(n)але це може зайняти деякий час для обчислення. Також ваш C-код роздруковує прості числа у міру знаходження, тоді як інші мови обчислюють їх у списки, а потім роздруковує. Хоча C, як не дивно, найшвидший, ви можете отримати його швидше.
Russ

2
(І, звичайно, Решето Ератосфена .. але цей мікро-орієнтир - це в значній мірі стрес-тест ітерацій та математичних операцій. Однак вони все ще не є "справедливими", як у деяких більш ліниві.)

2
Я просто запустив і свою версію Go, і вашу версію C (які дуже схожі), і я практично отримав однакову швидкість в обох. Я тільки спробував версію 100k: C: 2.723s Go: 2.743s.
Себастьян Гриньолі,

3
Вам не потрібно обчислювати sqrtдля цієї перевірки. Ви можете обчислити квадрат , iяк вfor (i = 2; i * i <= x; ++i) ...
ivant

3
Я пропоную вам анотувати в Scala оптимізовані isPrimeз @tailrec, щоб переконатися , що ви використовуєте хвостову рекурсію. Легко помилково зробити щось, що запобігає рекурсії хвоста, і ця анотація повинна попередити вас, якщо це станеться.
Daniel C. Sobral

Відповіді:


30

Грубі відповіді:

  1. Статичний набір тексту Scala тут йому дуже допомагає - це означає, що він використовує JVM досить ефективно, без зайвих зусиль.
  2. Я не зовсім впевнений у різниці Ruby / Python, але я підозрюю, що (2...n).all?функція is-prime?, ймовірно, буде досить добре оптимізована в Ruby (EDIT: звучить так, що це справді так, докладніше див. Відповідь Джуліана ...)
  3. Ruby 1.9.3 набагато краще оптимізований
  4. Код 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вперше, викликає ініціалізацію підсистем вводу-виводу або щось подібне!


2
Я не думаю, що те, що стосується Ruby та Python, обов'язково відповідає дійсності, але +1 інакше ..

Введення тексту не показало жодного вимірюваного стабільного результату, але ваш новий is-prime?показав покращення вдвічі. ;)
defhlt

чи не можна це зробити швидше, якби був непровірений мод?
Hendekagon

1
@Hendekagon - напевно! Не впевнений, наскільки це оптимізується поточним компілятором Clojure, можливо, є місце для вдосконалення. Clojure 1.4, безумовно, дуже допомагає загалом для такого роду речей, 1.5, мабуть, буде навіть кращим.
mikera

1
(zero? (mod n i))повинно бути швидше, ніж(= (mod n i) 0)
Йонас

23

Ось швидка версія 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 разів швидше, ніж ваш оригінал. На моїй машині це 100 тис. За 1,5 секунди.


2
Використання unchecked-remainder-intабо просто remзамість того, щоб modразом із результатами статичного набору тексту збільшити продуктивність у 4 рази Приємно!
defhlt

22

Я відповім лише №2, оскільки це єдиний, що я маю щось віддалено інтелектуальне сказати, але для вашого коду Python ви створюєте проміжний список у is_prime, тоді як ви використовуєте .mapу своєму allRuby, який просто ітерація.

Якщо ви зміните is_primeна:

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

вони на рівні.

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

EDIT: Не будучи надто безглуздим, роблячи код 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, він робить це .3 с для 10K і 21s для 100K.


1
Тут генератор має велике значення, оскільки дозволяє функції гарантувати перше False(хороший улов).
mgilson

Я дуже чекаю, що вони потраплять в PyPy ... Це буде неймовірно.
mgilson

Запустите, будь ласка, мою відповідь у PyPy? Мені цікаво, наскільки швидше це було б.
steveha

1
Ви абсолютно праві як щодо ітерації, так і xrange! Я виправив, і тепер Python і Ruby показують рівні результати.
defhlt

1
@steveha Я зроблю це, лише якщо ти обіцяєш зараз вийти і завантажити PyPy сам :)! pypy.org/download.html містить двійкові файли для всіх загальних ОС, і ваш менеджер пакетів, безсумнівно, їх має. У будь-якому випадку, що стосується вашого тесту, із випадковою lru_cacheреалізацією для 2.7, знайденою на AS, 100K працює за 2,3 секунди.
Джуліан

16

Ви можете зробити 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?


2
2x покращення. І гарне посилання!
defhlt

До речі, основний текст цього методу ідентичний i == n || n % i != 0 && isPrime(n, i + 1), який є коротшим, хоча і трохи важчим для читання
Луїджі Плінге

1
Вам слід було додати @tailrecанотацію, щоб переконатися, що вона здійснить таку оптимізацію.
Daniel C. Sobral

8

Ось моя версія 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: Згідно з пропозицією Еміля Х , я змінив свій код, щоб уникнути ефектів розминки вводу-виводу та 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)
  }
}

1
Чи впливає на код розминка jvm? Напр., isSexyPrimeМожливо, буде (більше) оптимізовано під час дзвінка findPrimesParта не стільки при дзвінкуfindPrimes
Еміль Х

@EmilH Досить справедливо. Я змінив свій код, щоб уникнути ефекту розминки io та jvm.
Eastsun

Тільки підняття до sqrt (n) - це хороша оптимізація, але тепер ви перевіряєте інший алгоритм.
Luigi Plinge

7

Не зважайте на орієнтири; проблема зацікавила мене, і я зробив кілька швидких налаштувань. Тут використовується 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") ця версія дала відповідь на 100 КБ менше ніж за 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.

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


lru_cacheбезумовно чудово. Для певних класів задач, таких як генерація послідовних чисел Фібоначчі, це може призвести до величезного прискорення, просто додавши один функціональний декоратор рядків! Ось посилання на виступ Реймонда Хеттінгера, якийlru_cache
триває

3
Використовуючи lru_cache, ви фактично використовуєте інший алгоритм, а не вихідний код. Отже, продуктивність стосується алгоритму, але не самої мови.
Eastsun

1
@Eastsun - я не розумію, що ти маєш на увазі. lru_cacheуникає повторення обчислень, які вже були зроблені нещодавно, і це все; Я не розумію, як це "насправді ми використовуємо інший алгоритм". І Python страждає від повільності, але виграє від того, що у нього є круті речі lru_cache; Я не бачу нічого поганого у використанні корисних частин мови. І я сказав, що не слід порівнювати час моєї відповіді з іншими мовами, не вносячи аналогічних змін до інших. Отже, я не розумію, що ви маєте на увазі.
steveha

@Eastsun має рацію, але, з іншого боку, має бути дозволено зручність мови вищого рівня, якщо не вказані додаткові обмеження. lru_cache жертвує пам'яттю для швидкості та регулює складність алгоритму.
Matt Joiner

2
якщо ви використовуєте інший алгоритм, ви можете спробувати Сіто Ератосфена. Версія Python дала відповідь на 100 тис. За 0.03лічені секунди ( 30мс) .
jfs

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

6

Я не зміг встояти, щоб зробити кілька найбільш очевидних оптимізацій для версії 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. Клієнтська або серверна ВМ не відрізняється особливою різницею, за винятком того, що навчання JIT, здається, трохи допомагає ВМ сервера (~ 3%), хоча це майже не впливає на клієнтську ВМ. Вихід у Java, здається, повільніший, ніж у C. Якщо вихід замінено статичним лічильником в обох версіях, версія Java працює трохи швидше, ніж версія C.

Ось мої часи для пробігу 100 тис.:

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

та пробіг 1М (16386 результатів):

  • 24,95 с C, складений за допомогою / Ox та статичного лічильника
  • Віртуальна машина клієнта Java 25.08s зі статичним лічильником
  • Віртуальна машина Java на 24,86 с із статичним лічильником

Хоча це насправді не відповідає на ваші запитання, воно показує, що невеликі налаштування можуть мати вагомий вплив на продуктивність. Тому, щоб мати змогу по-справжньому порівнювати мови, ви повинні намагатися якомога більше уникати всіх алгоритмічних відмінностей.

Це також дає підказку, чому Scala здається досить швидким. Він працює на віртуальній машині Java і, отже, виграє від його вражаючої продуктивності.


1
Швидше перейти до sqrt (x) замість x >> 1 для функції простої перевірки.
Єва Фрімен,

4

У Scala спробуйте використовувати Tuple2 замість List, це повинно йти швидше. Просто видаліть слово "Список", оскільки (x, y) є Tuple2.

Tuple2 спеціалізується на Int, Long і Double, що означає, що йому не доведеться встановлювати / розпаковувати ці необроблені типи даних. Джерело Tuple2 . Список не є спеціалізованим. Джерело списку .


Тоді ви не можете forallна це зателефонувати . Я також думав, що це може бути не найефективніший код (більше тому, що велика сувора колекція створюється для великих, nа не просто для використання подання), але він, звичайно, короткий + елегантний, і я був здивований, наскільки добре він працював, незважаючи на використання багато функціонального стилю.
0__

Ви маєте рацію, я думав, що "forAll" був там. Все-таки має бути значне покращення щодо Списку, і було б не так погано мати ці 2 дзвінки.
Tomas Lazaro

2
це справді швидше, тут def sexyPrimes(n: Int) = (11 to n).map(i => (i-6, i)).filter({ case (i, j) => isPrime(i) && isPrime(j) })він приблизно на 60% швидший, тому слід бити код С :)
0__

Хм, я отримую лише підвищення продуктивності на 4 або 5%
Луїджі Плінге

1
Я вважаю collectзначно повільнішим. Швидше, якщо спочатку зробити фільтр, а потім зіставити. withFilterтрохи швидше, оскільки насправді не створює проміжних колекцій. (11 to n) withFilter (i => isPrime(i - 6) && isPrime(i)) map (i => (i - 6, i))
Luigi Plinge

4

Ось код для версії 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 Мб кеш-пам'яті L2, 800 МГц FSB. 4 ГБ оперативної пам'яті

Версія 100 000: C: 2.723s Go: 2.743s

З 1000000 (1 млн замість 100 тис.): 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 (1 млн замість 100 тис.):

C: 3m35.458s Go: 3m36.259s Go using goroutines:3м27.137с2m16.125s

Версія 100 000:

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


Скільки ядер ви використовували до речі?
ом-ном-ном

2
У мене Asus u81a Intel Core 2 Duo T6500 2,1 ГГц, 2 Мб кеш-пам'яті L2, 800 МГц FSB. 4 ГБ оперативної пам'яті
Себастьян Гриньолі,

Ви насправді скомпілювали версію C із включеними оптимізаціями? Компілятор Go за замовчуванням не є вбудованим, і зазвичай він зазнає значного погіршення продуктивності порівняно з оптимізованим C у подібних порівняннях. Додати -O3або краще.
Matt Joiner

Я тільки що зробив, не раніше, і версія 100K зайняла стільки ж часу з -O3 або без неї
Себастьян Гриньолі

Те саме для версії 1M. Можливо, ці конкретні операції (ми тестуємо дуже малу підмножину) за замовчуванням добре оптимізовані.
Себастьян Гриньолі,

4

Просто для задоволення, ось паралельна версія 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,8 ГГц 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)

Схоже, JIT JVM дає Ruby приємний приріст продуктивності у випадку за замовчуванням, тоді як справжня багатопоточність допомагає JRuby виконувати роботу на 50% швидше у різьбовому корпусі. Що ще цікавіше, це те, що JRuby 1.7 покращує оцінку JRuby 1.6 на здорових 17%!


3

На основі відповіді x4u я написав версію масштабу , використовуючи рекурсію, і вдосконалив її, перейшовши лише до sqrt замість x / 2 для функції простої перевірки. Я отримую ~ 250 мс за 100 тис. І ~ 600 мс за 1 млн. Я пішов вперед і пішов на 10M за ~ 6 с.

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

2

Відповідь на ваше запитання №1 полягає в тому, що так, JVM неймовірно швидкий, і так, статичне введення допомагає.

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

Причини швидкості Java:

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

Завдяки статичному набору тексту, JVM може багато знати про ваш код під час компіляції, що дозволяє попередньо оптимізувати досить багато речей. Це також дозволяє компілятору оптимізувати кожен клас окремо, не знаючи, як інший клас планує його використовувати. Також Java не має довільних вказівників на розташування пам'яті, вона ЗНАЄ, які значення в пам'яті можуть змінюватися, а не змінювати, і може відповідно оптимізувати.

Розподіл купи набагато ефективніший, ніж C, розподіл купи Java швидше схожий на розподіл стеку C, але при цьому більш універсальний. Багато часу витрачено на різні алгоритми, які тут використовуються, це мистецтво - наприклад, усі об'єкти з коротким терміном життя (наприклад, змінні стека C) розподіляються у "відоме" вільне місце (пошук вільного місця немає) з достатньою кількістю місця) і всі вони звільняються в один крок (як стек).

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

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

До речі, більшість поганих показників швидкості Java виникають через тривалий час запуску JVM (колись хтось вбудує JVM в ОС, і це зникне!) Та того факту, що багато розробників справді погано пишуть Код графічного інтерфейсу (особливо різьбовий), що змусило графічні інтерфейси Java часто не реагувати та викликати неполадки. Прості у використанні мови, такі як Java та VB, мають свої помилки, посилені тим, що можливості середнього програміста, як правило, нижчі, ніж складніші мови.


Сказати, що розподіл купи JVM набагато ефективніший, ніж C, є нечутливим, враховуючи, що JVM написаний на C ++.
Daniel C. Sobral

5
@ DanielC.Sobral мова не настільки важлива, як імперментація - код реалізації Java "Heap" нічим не схожий на C. Java - це багатоступенчаста система, яку можна замінити, що оптимізується для різних цілей, завдяки численним людським зусиллям у дослідженнях, включаючи передові технології, що розробляються сьогодні, C використовує купу - просту структуру даних, розроблену багато століть тому. Систему Java неможливо реалізувати для C, враховуючи те, що C дозволяє вказівники, тому вона ніколи не може гарантувати "безпечні" переміщення довільних виділених фрагментів пам'яті без змін мови (що робить це вже не C)
Білл К

Безпека не має значення - ви не стверджували, що це безпечніше , ви стверджували, що вона більш ефективна . Крім того, ви описуєте в коментарі, як працює "купа", не має ніякого відношення до реальності.
Даніель К. Собрал,

Ви, мабуть, неправильно зрозуміли моє значення "безпечний" - Java може в будь-який час переміщати довільний блок пам'яті, оскільки знає, що може, C не в змозі оптимізувати все покриття пам'яті, оскільки може бути вказівник, який може посилатися на нього. Також купа змінного струму зазвичай реалізується як купа, яка є структурою даних. Кучки С ++ раніше реалізовувались із такими структурами купи, як C (отже, назва "Heap") Я не перевіряв C ++ протягом декількох років, тому це, можливо, вже не відповідає дійсності, але воно все ще обмежене неможливістю переорганізуйте невеликі фрагменти виділеної користувачем пам'яті за бажанням.
Bill K
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.