是否有任何“阈值”可以证明多线程计算的合理性?
是否有任何“阈值”可以证明多线程计算的合理性?
基本上,今天我需要优化这段代码。它试图找到由某个函数为前一百万个起始数字产生的最长序列:
public static void main(String[] args) { int mostLen = 0; int mostInt = 0; long currTime = System.currentTimeMillis(); for(int j=2; j<=1000000; j++) { long i = j; int len = 0; while((i=next(i)) != 1) { len++; } if(len > mostLen) { mostLen = len; mostInt = j; } } System.out.println(System.currentTimeMillis() - currTime); System.out.println("最长序列长度为 " + mostLen + " 对应的数字为 " + mostInt); } static long next(long i) { if(i%2==0) { return i/2; } else { return i*3+1; } }
我的错误在于尝试引入多线程:
void doSearch() throws ExecutionException, InterruptedException { final int numProc = Runtime.getRuntime().availableProcessors(); System.out.println("numProc = " + numProc); ExecutorService executor = Executors.newFixedThreadPool(numProc); long currTime = System.currentTimeMillis(); List> list = new ArrayList >(); for (int j = 2; j <= 1000000; j++) { MyCallable worker = new MyCallable (); worker.setBean(new ValueBean(j, 0)); Future f = executor.submit(worker); list.add(f); } System.out.println(System.currentTimeMillis() - currTime); int mostLen = 0; int mostInt = 0; for (Future f : list) { final int len = f.get().getLen(); if (len > mostLen) { mostLen = len; mostInt = f.get().getNum(); } } executor.shutdown(); System.out.println(System.currentTimeMillis() - currTime); System.out.println("最长序列长度为 " + mostLen + " 对应的数字为 " + mostInt); } public class MyCallable implements Callable { public ValueBean bean; public void setBean(ValueBean bean) { this.bean = bean; } public ValueBean call() throws Exception { long i = bean.getNum(); int len = 0; while ((i = next(i)) != 1) { len++; } return new ValueBean(bean.getNum(), len); } } public class ValueBean { int num; int len; public ValueBean(int num, int len) { this.num = num; this.len = len; } public int getNum() { return num; } public int getLen() { return len; } } long next(long i) { if (i % 2 == 0) { return i / 2; } else { return i * 3 + 1; } }
不幸的是,多线程版本在4个处理器(核心)上的运行速度比单线程版本慢了5倍。
然后我尝试了一种更简单的方法:
static int mostLen = 0; static int mostInt = 0; synchronized static void updateIfMore(int len, int intgr) { if (len > mostLen) { mostLen = len; mostInt = intgr; } } public static void main(String[] args) throws InterruptedException { long currTime = System.currentTimeMillis(); final int numProc = Runtime.getRuntime().availableProcessors(); System.out.println("numProc = " + numProc); ExecutorService executor = Executors.newFixedThreadPool(numProc); for (int i = 2; i <= 1000000; i++) { final int j = i; executor.execute(new Runnable() { public void run() { long l = j; int len = 0; while ((l = next(l)) != 1) { len++; } updateIfMore(len, j); } }); } executor.shutdown(); executor.awaitTermination(30, TimeUnit.SECONDS); System.out.println(System.currentTimeMillis() - currTime); System.out.println("最长序列长度为 " + mostLen + " 对应的数字为 " + mostInt); } static long next(long i) { if (i % 2 == 0) { return i / 2; } else { return i * 3 + 1; } }
它运行速度快得多,但仍然比单线程方法慢。
我希望这不是因为我搞砸了多线程的方式,而是因为这个特定的计算/算法不适合并行计算。如果我通过将方法next
替换为更加处理器密集型的计算来改变计算方式:
long next(long i) { Random r = new Random(); for(int j=0; j<10; j++) { r.nextLong(); } if (i % 2 == 0) { return i / 2; } else { return i * 3 + 1; } }
两个多线程版本在4核机器上的执行速度开始超过单线程版本的两倍以上。
因此,显然必须有一个阈值可以用来确定是否值得引入多线程进行优化(而不需要花费精力去实现它)。
我的问题是:
有哪些基本规则可以帮助决定给定的计算是否足够密集,以便通过并行计算进行优化?
有没有证明多线程计算的"阈值"?
问题出现的原因是并行化在单位工作互不依赖的情况下效果最好。当后续计算结果依赖于先前计算结果时,并行计算就不是最优的选择。依赖关系可以是强依赖,即"我需要第一个值来计算第二个值"。在这种情况下,任务完全是串行的,后续值无法在等待先前计算结果的情况下进行计算。也可以存在较弱的依赖关系,即"如果我有第一个值,我可以更快地计算第二个值"。在这种情况下,并行化的成本是可能会重复一些工作。
这个问题可以通过优化而不使用多线程来解决,因为如果您已经有了先前的结果,一些后续值可以更快地计算出来。例如,考虑 j == 4
。内部循环一次产生 i == 2
,但是两次迭代前刚计算了 j == 2
的结果,如果保存了 len
的值,可以计算出 len(4) = 1 + len(2)。
使用数组来存储先前计算过的 len
值,并在 next
方法中进行一些修改,可以使任务完成速度提高超过50倍。
是的,这比批量多线程的1000倍还要快!我想知道是否可以将这个问题多线程化。
这是可能的。我会研究一下 ConcurrentHashMap
,这样我就可以在构建缓存时不必担心锁问题。虽然我认为数组实现非常快,因为只要 i < j
,就知道它在缓存中,但是哈希查找可能会慢很多。如果您可以利用 next
函数的其他数学属性,很容易证明对于一个限制 n,具有最长长度的 j 必须满足 j > n / 2。这有助于多线程解决方案,但不适用于缓存解决方案。此外,简单的数组缓存无法扩展到限制大于约 42,000,000 的情况。
多线程计算的效率与成本有着密切的关系,因此需要找到一个合适的阈值来确定是否使用多线程计算。没有固定的规则来确定这个阈值,因为它很大程度上取决于硬件。
启动和停止线程的成本很高。当然,你已经使用了执行器服务(executor service),它使用一组工作线程来执行你的Runnable,从而大大减少了这些成本。然而,每个Runnable仍然带有一些开销。减少Runnable的数量并增加每个Runnable需要执行的工作量将提高性能,但仍需要足够的Runnable来让执行器服务有效地将它们分配到工作线程上。
你选择为每个起始值创建一个Runnable,因此你最终会创建1000000个Runnable。如果让每个Runnable处理一批,比如1000个起始值,可能会得到更好的结果。这意味着你只需要1000个Runnable,大大减少了开销。
使用批处理的方式可以减少1000000个任务的开销,因为线程没有工作可做时会导致生产力下降。