使用Ruby线程,但没有看到加速

时间:2013-12-21 00:07:32

标签: ruby multithreading

我正在尝试为Project Euler的问题14编写一个多线程解决方案,但我并没有真正看到加速。没有任何共享资源,也没有使用Mutex锁...由于上下文切换,我的代码是否很慢?我没有正确理解线程的好处吗?

http://projecteuler.net/problem=14

先谢谢!

require 'benchmark'

benchmark_results = Benchmark.measure do
  threads = []
  num_threads = 10

  num_threads.times do |thread_num|
    threads << Thread.new(thread_num + 1) do |thread_num|
      Thread.current["max_length"] = 0
      (thread_num..1000000).step(num_threads).each do |i|
        next if i.even?
        current = i
        length = 0

        until current == 1
          if current.even?
            current = current / 2
          else
            current = current * 3 + 1
          end
          length += 1
        end

        if length > Thread.current["max_length"]
          Thread.current["max_length"] = length
          Thread.current["max_i"] = i
        end
      end
    end
  end

  threads.each { |thread| thread.join; print "#{thread['max_i']} -> #{thread['max_length']}\n" }
end

puts benchmark_results

4 个答案:

答案 0 :(得分:2)

据我所知,大多数ruby实现不使用实际线程(操作系统级别)或仅使用某种锁定,因此这些实现将无法从多个内核/处理器线程中受益。 (见http://en.wikibooks.org/wiki/Ruby_Programming/Reference/Objects/Thread

如果您的应用程序受CPU限制,那么应该有效地阻止您从线程中获益。另一方面,如果它是IO绑定,则线程可能会有所帮助。 (然后,当一个绿色线程等待IO时,其他绿色线程可以使用您分配的CPU时间。但是,在物理上,仍然只使用一个CPU内核。)

答案 1 :(得分:1)

Ruby有许多不同的实现。最常提到的是MRI(见other question

MRI有线程,但遗憾的是一次只使用一个CPU核心。这意味着:当时只有一个线程实际运行。

如果你的线程必须等待IO发生,那么可能会加速。因为如果一个线程必须等待,另一个线程可以赶上。但是你的问题一直需要CPU。

我建议调查另一个像JRuby这样的Ruby实现来解决这类问题。 JRuby有真正的线程。

如果您更改实施,也许您的速度会更快。在你重复计算每一max_length次的那一刻。例如:n = 4的序列长度为3。如果您为length计算n = 8,则执行一步(n / 2)而不是current 4,您就会知道n = 4 1}}有length = 3:因此length(8) = 1 + length(4) = 1 + 4 = 5。例如:

class CollatzSequence

  def initialize
    @lengths = Hash.new { |h, n| cache_length(h, n) }
  end

  def length(n)
    @lengths[n]
  end

private

  def cache_length(h, n)
    if n <= 1
      h[n] = 1
    else
      next_in_seqence = n.even? ? (n / 2) : (n * 3 + 1)
      h[n] = 1 + h[next_in_seqence]
    end
  end

end

require 'benchmark'
sequencer = CollatzSequence.new

Benchmark.bm(10) do |bm| 
  bm.report('not cached')  { sequencer.length(837799)     } 
  bm.report('cache hit 1') { sequencer.length(837799)     } 
  bm.report('cache hit 2') { sequencer.length(837799 * 2) } 
end

#                  user     system      total        real
# not cached   0.000000   0.000000   0.000000 (  0.001489)
# cache hit 1  0.000000   0.000000   0.000000 (  0.000007)
# cache hit 2  0.000000   0.000000   0.000000 (  0.000011)

答案 2 :(得分:1)

您需要的只是一个过程,从奇数小于1000000开始查找最长的Collat​​z链,另一个查找从偶数小于1000000开始的最长的一个。在单独的核心中运行多个脚本实例如果你手动启动它们并不太难。它既便宜又脏,但它的工作原理:-)但它们不能只是一个进程中的线程,它们必须是独立的进程。 (我认为我称之为“进程”的是ThorX89所谓的“OS线程”。)

答案 3 :(得分:0)

除了线程之外,还有其他方法可以加快速度。

我似乎记得Ruby是一种特别慢的语言,由一个不关心性能的人设计。也许你可以使用不同的语言。

更重要的是:你这是天真的做法。它有效,但很多计算都重复了很多次。例如,您有以下Collat​​z序列:

         7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1
                11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1
29, 88, 44, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1

序列中的大多数步骤(例如52-> 26)被计算不止一次。这里有一些优化空间。您已经通过忽略以偶数开头的序列进行了一些优化(顺便说一下,在整理结果时忘记纠正这个问题)。我发现了一种更快的方法,并将其与天真的方法进行了比较。对于前10,000个而不是前1,000,000个,天真的方法需要63秒;忽略偶数的天真方法耗时35秒;我的方法花了5秒钟。对于完整的1,000,000,我的算法花了9分钟;我没有尝试运行其他人。 (所有这些都是用Perl编写的。)如果你不想要了解我如何做的更多细节,请立即离开!

我不是仅计算每个结果然后忘记它,而是在我进行的时候制作了一系列结果。现在假设您计算了所有Collat​​z序列长度,最多为12.要计算从13开始的序列长度,从13开始并继续,直到达到小于13的数字。序列为13,40,20 ,10。你看一下数组中的元素10,发现从10到1是6步。你知道从13到10是3步,因为你刚刚完成了这些步骤。因此从13到1是9步,因此您将9设置为数组中的元素13.

用线程做这个没有明显/好的方法。如果你真的想要的话我觉得有可能提出一些东西,但这不值得努力。也许如果他们说了十亿......