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 主机) 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] - 恐怕要花多少时间

代码清单

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"

斯卡拉:

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 代码吗?会有帮助吗?
4

13 回答 13

30

粗略的回答:

  1. Scala 的静态类型在这里帮了很大的忙——这意味着它可以非常有效地使用 JVM,而无需太多额外的努力。
  2. 我不太确定 Ruby/Python 的区别,但我怀疑(2...n).all?该函数is-prime?可能在 Ruby 中得到了很好的优化(编辑:听起来确实如此,有关更多详细信息,请参阅 Julian 的答案......)
  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 在 0.635 秒内完成了 10k(即你列表中第二快的,击败了 Scala)

PS请注意,在某些情况下,您在基准测试中打印了代码 - 这不是一个好主意,因为它会扭曲结果,特别是如果print第一次使用类似的函数会导致 IO 子系统的初始化或类似的东西!

于 2012-07-25T01:12:14.563 回答
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 中新的 reducers 库的版本(需要 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 倍。在我的机器上,1.5 秒内达到 100k。

于 2012-07-25T05:19:04.393 回答
22

我将只回答 #2,因为它是唯一一个我有任何远程智能要说的东西,但是对于你的 Python 代码,你正在创建一个中间列表 in is_prime,而你.map在你的allin 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 运行它时 10K 的速度为 0.3 秒,100K 的速度为 21 秒。

于 2012-07-25T00:35:12.037 回答
16

isPrime您可以通过修改您的方法使 Scala 更快

  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 中优化 for-comprehensions 和循环?

于 2012-07-25T01:42:49.560 回答
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))
  }
}

编辑:根据Emil H的建议,我更改了代码以避免 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)
  }
}
于 2012-07-25T06:47:31.273 回答
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,但如果您安装提供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 GHz AMD Turion II “K625”)这个版本在 8 秒内产生了 100K 的响应。

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 中会更痛苦。

如果不重写其他版本以使用类似的技巧,则无法将此版本上的数字与其他版本的数字进行比较。我不想在这里证明什么。我只是觉得这个问题很有趣,我想看看我能收集到什么样的简单性能改进。

于 2012-07-25T01:53:36.257 回答
7

不要忘记 Fortran!(主要是在开玩笑,但我希望与 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
于 2012-07-25T12:01:34.773 回答
6

我忍不住对 C 版本进行了一些最明显的优化,这使得 100k 测试现在在我的机器上花费了 0.3 秒(比问题中的 C 版本快 5 倍,均使用 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 版本快一点。

这些是我跑 100k 的时间:

  • 319ms C 用 /Ox 编译并输出到 >NIL:
  • 312ms C 用 /Ox 和静态计数器编译
  • 324 毫秒 Java 客户端 VM,输出为 >NIL:
  • 带有静态计数器的 299 毫秒 Java 客户端 VM

和 1M 运行(16386 个结果):

  • 24.95s C 用 /Ox 和静态计数器编译
  • 25.08s Java 客户端 VM 与静态计数器
  • 带静态计数器的 24.86 秒 Java 服务器 VM

虽然这并不能真正回答您的问题,但它表明小的调整会对性能产生显着影响。因此,为了能够真正比较语言,您应该尽量避免所有算法差异。

它还暗示了为什么 Scala 看起来相当快。它在 Java VM 上运行,因此受益于其令人印象深刻的性能。

于 2012-07-25T14:39:36.193 回答
4

在 Scala 中尝试使用 Tuple2 而不是 List,它应该会更快。只需删除单词“List”,因为 (x, y) 是 Tuple2。

Tuple2 专门用于 Int、Long 和 Double,这意味着它不必对这些原始数据类型进行装箱/拆箱。元组 2 源。列表不是专门的。列出源

于 2012-07-25T00:45:19.577 回答
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 版本一样快。

使用华硕 u81a Intel Core 2 Duo T6500 2.1GHz、2MB L2 缓存、800MHz FSB。4GB 内存

100k 版本:C: 2.723s Go: 2.743s

使用 1000000(1M 而不是 100K):C: 3m35.458s Go: 3m36.259s

但我认为使用 Go 内置的多线程功能并将该版本与常规 C 版本(没有多线程)进行比较是公平的,因为用 Go 进行多线程几乎太容易了。

更新:我在 Go 中使用 Goroutines 做了一个并行版本:

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 Mb 的 RAM。

一件奇怪的事情:我的双核 kubuntu 64 位从未在两个内核中达到峰值。看起来 Go 只使用了一个核心。通过调用修复runtime.GOMAXPROCS(4)

更新:我运行了多达 100 万个数字的并行版本。 我的一个 CPU 内核一直处于 100% 状态,而另一个根本没有使用(奇怪)。它比 C 和常规 Go 版本多花了一分钟。:(

使用 1000000(1M 而不是 100K):

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

100k 版本:

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

于 2012-07-25T04:32:18.977 回答
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.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 在默认情况下为 Ruby 提供了不错的性能提升,而真正的多线程帮助 JRuby 在线程情况下的执行速度提高了 50%。更有趣的是,JRuby 1.7 将 JRuby 1.6 的分数提高了 17%!

于 2012-07-25T13:17:00.867 回答
3

基于x4u 的回答,我使用递归编写了一个 scala 版本,并且我通过仅使用 sqrt 而不是 x/2 进行质数检查功能对其进行了改进。100k 大约 250 毫秒,1M 大约 600 毫秒。我继续前进,在约 6 秒内达到 10M。

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) 版本,通过使用计数器(忽略 I/O),100k 大约为 15ms,1M 为 250ms,10M 为 6s。如果我打开输出,100k 需要 150ms,1M 需要 1s,10M 需要 12s。不幸的是,这里不能使用尾递归,所以我不得不将它转换回循环。

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")
于 2012-07-25T19:19:30.513 回答
2

您的问题 #1 的答案是,是的,JVM 速度非常快,是的,静态类型有帮助。

从长远来看,JVM 应该比 C 更快,甚至可能比“普通”汇编语言更快——当然,您总是可以通过手动运行时分析并为每个 CPU 创建单独的版本来手动优化汇编以击败任何东西,您只需必须非常优秀和知识渊博。

Java速度快的原因是:

JVM 可以在运行时分析您的代码并对其进行手动优化——例如,如果您有一个可以在编译时静态分析为真正函数的方法,并且 JVM 注意到您经常以相同的方式调用它参数,它实际上可以完全消除调用,只注入最后一次调用的结果(我不确定Java是否真的做到了这一点,但它确实做了很多这样的事情)。

由于静态类型,JVM 可以在编译时了解很多关于你的代码的信息,这让它可以预先优化很多东西。它还允许编译器单独优化每个类,而无需知道另一个类计划如何使用它。此外,Java 没有指向内存位置的任意指针,它知道内存中的哪些值可能会更改,也可能不会更改,并且可以相应地进行优化。

堆分配比 C 更有效,Java 的堆分配在速度上更像 C 的堆栈分配——但更通用。很多时间都花在了这里使用的不同算法上,这是一门艺术——例如,所有生命周期短的对象(如 C 的堆栈变量)都被分配到一个“已知的”空闲位置(无需搜索空闲位置)有足够的空间)并在一个步骤中一起释放(如堆栈弹出)。

JVM 可以了解您的 CPU 架构的怪癖,并专门为给定的 CPU 生成机器代码。

JVM 可以在您交付代码后很长时间内加速您的代码。就像将程序移到新的 CPU 可以加快速度一样,将其移到新版本的 JVM 也可以为您提供巨大的速度性能,这些性能在您最初编译代码时甚至不存在的 CPU 上,而 c 物理上不能做没有重新编译。

顺便说一句,Java 速度的大部分不良代表来自加载 JVM 的较长启动时间(有一天有人会将 JVM 构建到操作系统中,这将消失!)以及许多开发人员真的不擅长编写的事实GUI 代码(尤其是线程化的)导致 Java GUI 经常变得无响应和故障。Java 和 VB 等简单易用的语言由于普通程序员的能力往往低于更复杂的语言这一事实​​而放大了它们的缺陷。

于 2012-07-25T17:44:25.043 回答