如何优化这一段ruby代码以加快速度?

时间:2009-04-20 15:37:28

标签: ruby optimization

这是Sieve of Eratosthenes的实现。

class PrimeGenerator
  def self.get_primes_between( x, y)
    sieve_array = Array.new(y) {|index|
      (index == 0 ? 0 : index+1)
    }

    position_when_we_can_stop_checking = Math.sqrt(y).to_i
    (2..position_when_we_can_stop_checking).each{|factor|
      sieve_array[(factor).. (y-1)].each{|number|
        sieve_array[number-1] = 0 if isMultipleOf(number, factor)
      }
    }

    sieve_array.select{|element| 
      ( (element != 0) && ( (x..y).include? element) )
    }
  end
  def self.isMultipleOf(x, y)
    return (x % y) == 0
  end
end

现在我这样做是为了'提交解决问题的解决方案,因为你有时间杀死'网站。我选择红宝石作为我的impl语言..然而我被宣布超时。 我做了一些基准测试

require 'benchmark'
Benchmark.bmbm do |x|
  x.report ("get primes") { PrimeGenerator.get_primes_between(10000, 100000)}
  end

ruby​​ 1.9.1p0(2009-01-30修订版21907)[i386-mswin32]

L:\Gishu\Ruby>ruby prime_generator.rb
Rehearsal ----------------------------------------------
get primes  33.953000   0.047000  34.000000 ( 34.343750)
------------------------------------ total: 34.000000sec

                 user     system      total        real
get primes  33.735000   0.000000  33.735000 ( 33.843750)

ruby​​ 1.8.6(2007-03-13 patchlevel 0)[i386-mswin32]

Rehearsal ----------------------------------------------
get primes  65.922000   0.000000  65.922000 ( 66.110000)
------------------------------------ total: 65.922000sec

                 user     system      total        real
get primes  67.359000   0.016000  67.375000 ( 67.656000)

所以我在C#2.0 / VS 2008中重新编写了这个东西 - > 722毫秒

所以现在这让我觉得我的实现是一个问题,还是这种语言之间的差异? (我对1.9 Ruby Ruby感到惊讶......直到我不得不将它与C#进行比较:)

更新 结果是我的“put-eratosthenes-to-shame-adapt”毕竟:) 消除不必要的循环迭代是主要的优化。如果有人对细节感兴趣,你可以阅读here;反正这个问题太长了。

6 个答案:

答案 0 :(得分:4)

我首先看一下你的内循环。 sieve_array[(factor).. (y-1)]每次执行时都会创建一个新数组。相反,尝试用普通的索引循环替换它。

答案 1 :(得分:4)

显然,每台计算机都会以不同的方式对其进行基准测试,但是通过使用每个块删除数组上的循环,并通过引起内部,我能够在我的机器(Ruby 1.8.6)上使运行速度提高约50倍。循环检查更少的数字。

factor=2
while factor < position_when_we_can_stop_checking 
    number = factor
    while number < y-1
      sieve_array[number-1] = 0 if isMultipleOf(number, factor)
      number = number + factor; # Was incrementing by 1, causing too many checks
    end
  factor = factor +1
end

答案 2 :(得分:2)

Eratosthenes的Sieve作为寻找素数的说明性方法很好,但我会实现它有点不同。实质是你不必检查已知素数的倍数的数字。现在,您可以创建一个列表,列出所有顺序素数的列表,直到您正在检查的数字的平方根,然后只需通过素数列表来检查素数就足够了,而不是使用数组来存储这些信息。

如果你想到它,这就是你对图像所做的,但是采用更“虚拟”的方式。

修改:快速入侵我的意思(不是从网上复制而来);)

  public class Sieve {
    private readonly List<int> primes = new List<int>();
    private int maxProcessed;

    public Sieve() {
      primes.Add(maxProcessed = 2); // one could add more to speed things up a little, but one is required
    }

    public bool IsPrime(int i) {
      // first check if we can compare against known primes
      if (i <= primes[primes.Count-1]) {
        return primes.BinarySearch(i) >= 0;
      }
      // if not, make sure that we got all primes up to the square of i
      int maxFactor = (int)Math.Sqrt(i);
      while (maxProcessed < maxFactor) {
        maxProcessed++;
        bool isPrime = true;
        for (int primeIndex = 0; primeIndex < primes.Count; primeIndex++) {
          int prime = primes[primeIndex];
          if (maxProcessed % prime == 0) {
            isPrime = false;
            break;
          }
        }
        if (isPrime) {
          primes.Add(maxProcessed);
        }
      }
      // now apply the sieve to the number to check
      foreach (int prime in primes) {
        if (i % prime == 0) {
          return false;
        }
        if (prime > maxFactor) {
          break;
        }
      }
      return true;
    }
  }

在我的慢速机器上使用大约67毫秒....测试应用程序:

class Program {
    static void Main(string[] args) {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        Sieve sieve = new Sieve();
        for (int i = 10000; i <= 100000; i++) {
            sieve.IsPrime(i);
        }
        sw.Stop();
        Debug.WriteLine(sw.ElapsedMilliseconds);
    }
}

答案 3 :(得分:2)

我不知道它如何比较速度,但这是一个相当小而简单的SoE实现,对我来说很好用:

def sieve_to(n)
  s = (0..n).to_a
  s[0]=s[1]=nil
  s.each do |p|
    next unless p
    break if p * p > n
    (p*p).step(n, p) { |m| s[m] = nil }
  end
  s.compact
end

还有一些可能的加速,但我认为它非常好。

它们不完全等效,所以你的10_000到1_000_000等于

sieve_to(1_000_000) - sieve_to(9_999)

或近似的东西。

无论如何,在WinXP上,使用Ruby 1.8.6(和相当大的Xeon CPU),我得到了这个:

require 'benchmark'
Benchmark.bm(30) do |r|
  r.report("Mike") { a = sieve_to(10_000) - sieve_to(1_000) } 
  r.report("Gishu") { a = PrimeGenerator.get_primes_between( 1_000, 10_000) }
end

给出了

                                    user     system      total        real
Mike                            0.016000   0.000000   0.016000 (  0.016000)
Gishu                           1.641000   0.000000   1.641000 (  1.672000)

(我因为无聊等待而停止了一百万个案件的运行。)

所以我说这是你的算法。 ; - )

C#解决方案几乎可以保证快几个数量级。

答案 4 :(得分:0)

我还要注意,根据我的经验,Ruby在Windows系统上比在* nix上慢得多。当然,我不确定你有什么速度处理器,但是在Ruby 1.9中我的Ubuntu盒子上运行这段代码需要大约10秒,而1.8.6需要30秒。

答案 5 :(得分:0)

用ruby-prof对它进行基准测试。它可以吐出像kcachegrind这样的工具,可以查看代码缓慢的位置。

然后,一旦你快速制作ruby,使用RubyInline为你优化方法。