【问题标题】:Unexpected multithreaded result意外的多线程结果
【发布时间】:2009-02-06 04:24:51
【问题描述】:

我编写了几个 Java 类 —SingleThreadedComputeMultithreadedCompute — 来证明一个事实(或者我一直认为的事实!),如果您在单核机器,你不会得到加速。事实上,我的理解是,并行化此类任务实际上会减慢速度,因为现在您必须处理上下文切换开销。好吧,我运行了这些类,并且并行版本出乎意料地运行得更快:单线程版本在我的机器上始终以略多于 7 秒的速度运行,而多线程版本在我的机器上始终以略多于 6 秒的速度运行。谁能解释一下这是怎么可能的?

如果有人想亲自查看或尝试,这里是这些课程。

public final class SingleThreadedCompute {
    private static final long _1B = 1000000000L; // one billion

    public static void main(String[] args) {
        long startMs = System.currentTimeMillis();

        long total = 0;
        for (long i = 0; i < _1B; i++) { total += i; }
        System.out.println("total=" + total);

        long elapsedMs = System.currentTimeMillis() - startMs;
        System.out.println("Elapsed time: " + elapsedMs + " ms");
    }
}

这是多线程版本:

public final class MultithreadedCompute {
    private static final long _1B = 1000000000L; // one billion
    private static final long _100M = _1B / 10L;

    public static void main(String[] args) {
        long startMs = System.currentTimeMillis();

        System.out.println("Creating workers");
        Worker[] workers = new Worker[10];
        for (int i = 0; i < 10; i++) {
            workers[i] = new Worker(i * _100M, (i+1) * _100M);
        }

        System.out.println("Starting workers");
        for (int i = 0; i < 10; i++) { workers[i].start(); }

        for (int i = 0; i < 10; i++) {
            try {
                workers[i].join();
                System.out.println("Joined with thread " + i);
            } catch (InterruptedException e) { /* can't happen */ }
        }

        System.out.println("Summing worker totals");
        long total = 0;
        for (int i = 0; i < 10; i++) { total += workers[i].getTotal(); }
        System.out.println("total=" + total);

        long elapsedMs = System.currentTimeMillis() - startMs;
        System.out.println("Elapsed time: " + elapsedMs + " ms");
    }

    private static class Worker extends Thread {
        private long start, end;
        private long total;

        public Worker(long start, long end) {
            this.start = start;
            this.end = end;
        }

        public void run() {
            System.out.println("Computing sum " + start + " + ... + (" + end + " - 1)");
            for (long i = start; i < end; i++) { total += i; }
        }

        public long getTotal() { return total; }
    }
}

这是运行单线程版本的输出:

total=499999999500000000
Elapsed time: 7031 ms

这是运行多线程版本的输出:

Creating workers
Starting workers
Computing sum 0 + ... + (100000000 - 1)
Computing sum 100000000 + ... + (200000000 - 1)
Computing sum 200000000 + ... + (300000000 - 1)
Computing sum 300000000 + ... + (400000000 - 1)
Computing sum 400000000 + ... + (500000000 - 1)
Computing sum 500000000 + ... + (600000000 - 1)
Computing sum 600000000 + ... + (700000000 - 1)
Computing sum 700000000 + ... + (800000000 - 1)
Computing sum 800000000 + ... + (900000000 - 1)
Computing sum 900000000 + ... + (1000000000 - 1)
Joined with thread 0
Joined with thread 1
Joined with thread 2
Joined with thread 3
Joined with thread 4
Joined with thread 5
Joined with thread 6
Joined with thread 7
Joined with thread 8
Joined with thread 9
Summing worker totals
total=499999999500000000
Elapsed time: 6172 ms

编辑:环境信息:

  • Microsoft Windows XP 专业版 2002,SP3
  • 戴尔精密 670
  • 英特尔至强 CPU 2.80GHz,1 MB 二级缓存

不知道如何证明它是单核机器,除了说明上面的规格并指出当我购买机器时(2005 年 8 月),单核是标准的,我没有升级到多核(如果那甚至是一种选择……我不记得了)。如果在 Windows 中的某个地方我可以检查系统属性(显示上述信息)以外的其他地方,请告诉我,我会检查。


以下是连续五次 ST 和 MT 运行:

五个单线程运行:

总计=499999999500000000 经过时间:7000 毫秒

总计=499999999500000000 经过时间:7031 毫秒

总计=499999999500000000 经过时间:6922 毫秒

总计=499999999500000000 经过时间:6968 毫秒

总计=499999999500000000 经过时间:6938 毫秒


五个多线程运行:

总计=499999999500000000 经过时间:6047 毫秒

总计=499999999500000000 经过时间:6141 毫秒

总计=499999999500000000 经过时间:6063 毫秒

总计=499999999500000000 经过时间:6282 毫秒

总计=499999999500000000 经过时间:6125 毫秒

【问题讨论】:

  • 首先,向我们展示 5 次连续运行以及单线程和多线程的运行时间。然后确保我们您确实有一个单核 CPU。
  • 我没有windows,也没有单核机,所以不敢尝试,但我赌了一小笔钱,长期运行的任务正在输优先事项;我认为这是一个调度程序问题。
  • @SWMonkey,我说过要关闭 JIT,因为它可能会以不同方式编译两段代码。最好将其从方程式中完全删除。
  • Pax,发布您的“关闭 JIT”答案作为答案(而不是评论),以便我可以接受它...我将发布非 JIT 运行结果...

标签: java multithreading concurrency


【解决方案1】:

这可能是由于超线程和/或流水线造成的。

来自维基百科on hyper-threading

超线程是超线程的进步。超线程(官方称为超线程技术或 HTT)是一种英特尔专有技术,用于改进在 PC 微处理器上执行的计算(一次执行多个任务)的并行化。启用了超线程的处理器被操作系统视为两个处理器而不是一个。这意味着物理上只有一个处理器,但操作系统看到两个虚拟处理器,并在它们之间分担工作负载。

来自维基百科on piplining

在计算中,管道是一组串联的数据处理元素,因此一个元素的输出就是下一个元素的输入。管道的元素通常以并行或时间片方式执行

【讨论】:

  • 我同意 Software Monkey,听起来像超线程,你应该可以在 BIOS 中禁用/启用它
  • 是的,我记得当我有 Pentium IV HT 时,它在操作系统中显示了 2 个内核。
【解决方案2】:

您的其他环境是什么样的?这是可重复的吗?

至少在 UNIX 机器上,像这样长时间运行的单个进程的优先级可能会降低;如果你有 10 个线程,每个线程都有自己的 CPU 部分,因此不会累积太多的 CPU 时间。这样它就不会失去对精细化的优先权。总体而言,它获得了更大的总 CPU 块。

添加

为了完整起见,这是您的代码在 OS/X 10.5.6 下的双核 mac mini 上提供的内容

527 $ java MultithreadedCompute
Creating workers
Starting workers
Computing sum 100000000 + ... + (200000000 - 1)
Computing sum 0 + ... + (100000000 - 1)
Computing sum 400000000 + ... + (500000000 - 1)
Computing sum 200000000 + ... + (300000000 - 1)
Computing sum 500000000 + ... + (600000000 - 1)
Computing sum 600000000 + ... + (700000000 - 1)
Computing sum 700000000 + ... + (800000000 - 1)
Computing sum 800000000 + ... + (900000000 - 1)
Computing sum 900000000 + ... + (1000000000 - 1)
Computing sum 300000000 + ... + (400000000 - 1)
Joined with thread 0
Joined with thread 1
Joined with thread 2
Joined with thread 3
Joined with thread 4
Joined with thread 5
Joined with thread 6
Joined with thread 7
Joined with thread 8
Joined with thread 9
Summing worker totals
total=499999999500000000
Elapsed time: 3217 ms
528 $ java SingleThreadedCompute
total=499999999500000000
Elapsed time: 5651 ms
529 $ 

如你所见,线程不一定顺序运行,多线程的运行时间约为单线程的56%,说明它正在利用线程。

【讨论】:

    【解决方案3】:

    我尝试按照上面评论中 Pax 的建议关闭 JIT。 Pax,如果您想发布一个快速的“关闭 JIT”答案,我会感谢您的解决方案。

    无论如何,关闭 JIT 是有效的(意味着它使实际结果与预期结果一致)。我不得不放弃 10 亿,因为它会永远持续下去,所以我选择了 1 亿。结果更符合我的预期。他们在这里:

    五个 NO-JIT 单线程运行

    总计=4999999950000000 经过时间:17094 毫秒

    总计=4999999950000000 经过时间:17109 毫秒

    总计=4999999950000000 经过时间:17219 毫秒

    总计=4999999950000000 经过时间:17375 毫秒

    总计=4999999950000000 经过时间:17125 毫秒


    五次无 JIT 多线程运行

    总计=4999999950000000 经过时间:18719 毫秒

    总计=4999999950000000 经过时间:18750 毫秒

    总计=4999999950000000 经过时间:18610 毫秒

    总计=4999999950000000 经过时间:18890 毫秒

    总计=4999999950000000 经过时间:18719 毫秒


    感谢大家的想法和帮助。

    【讨论】:

    • 是的,但是为什么机器码不同;在两个版本上都使用 JIT 进行了编译。这里有一些重要的东西。实际上,JIT 案例在现实生活中很重要。
    • 之所以不同,是因为这两个程序本身是不同的。当 JIT 认为这样做有好处时,它就会介入。似乎它更早(无论出于何种原因)与 MT 程序一起启动。
    • 好吧,我想等待 Pax 发布他的正确答案,这样我才能相信它,但他没有,所以我会接受这个(这确实是他的答案)。 :-D
    【解决方案4】:

    十分之一秒的差异?启动时的噪音(单独)会淹没它。写一些运行一两分钟的东西。

    【讨论】:

    • 嗯,这是一秒的差异(或者在无 JIT 的情况下大约为 1.5 秒),但我会试一试。在 JIT 案例中,我希望看到 ST 和 MT 运行之间出现一些收敛,因为 JIT 运行时间占整体运行时间的百分比增加了。让我尝试一下。一秒。
    • 嗨,保罗。我将总和重新设置为 1B(而不是 100M)并重新运行。我只为 ST 和 MT 分别做了一个样本,但在这里它们是:ST=180281 ms; MT=186516 毫秒。 ST 快了 6 秒以上,这与我最初宣布的预期一致。
    【解决方案5】:

    尝试消除由单线程和多线程变体执行的代码之间因 HotSpot 引起的差异:

    public class ThreadedWorkers {
        private static final long _1B = 1000000000L; // one billion
        private static final long _100M = _1B / 10L;
    
        enum ThreadMode { SINGLE, SEQUENTIAL, MULTI };
    
        public static void main(String[] args) throws InterruptedException {
            final long startMs = System.currentTimeMillis();
    
            ThreadMode mode = args.length == 0 ? ThreadMode.SINGLE : ThreadMode.valueOf(args[0].toUpperCase());
    
            final long total = computeTotal( mode );
    
            System.out.println("total=" + total);
    
            long elapsedMs = System.currentTimeMillis() - startMs;
    
            System.out.println("Elapsed time: " + elapsedMs + " ms");
        }
    
        public static long computeTotal (ThreadMode mode) throws InterruptedException {
            Worker[] workers = new Worker[10];
    
            for (int i = 0; i < 10; i++)
                workers[i] = new Worker(i * _100M, (i+1) * _100M);
    
            switch (mode) {
                case SINGLE: {
                    for (Worker worker : workers )
                        worker.run();
    
                    break;
                } 
    
                case SEQUENTIAL:{
                    for (Worker worker : workers ) {
                        worker.start();
                        worker.join();
                    }
    
                    break;
                }
    
                case MULTI: {
                    for (Worker worker : workers )
                        worker.start();
    
                    for (Worker worker : workers )
                        worker.join();
    
                    break;
                }
            }
    
            System.out.println("Summing worker totals");
    
            long total = 0;
    
            for (Worker worker : workers )
                total += worker.getTotal();
    
            return total;
        }
    
        static class Worker extends Thread {
            private long start, end, total;
    
            public Worker(long start, long end) {
                this.start = start;
                this.end = end;
            }
    
            public void run() {
                System.out.println("Computing sum " + start + " + ... + (" + end + " - 1)");
                for (long i = start; i < end; i++) { total += i; }
            }
    
            public long getTotal() { return total; }
        }
    }
    

    这仍然比顺序或单一运行更快(在 eee pc 900 - 23 与 13 秒上大约 10 秒),即使顺序执行与 multi 相同次数相同的方法。

    【讨论】:

      【解决方案6】:

      仅仅因为它很有趣……来自 8 核服务器级机器的结果。 AMD 2.7GHz上海cpu

      Creating workers
      Starting workers
      Computing sum 0 + ... + (100000000 - 1)
      Computing sum 100000000 + ... + (200000000 - 1)
      Computing sum 300000000 + ... + (400000000 - 1)
      Computing sum 500000000 + ... + (600000000 - 1)
      Computing sum 600000000 + ... + (700000000 - 1)
      Computing sum 200000000 + ... + (300000000 - 1)
      Computing sum 800000000 + ... + (900000000 - 1)
      Computing sum 700000000 + ... + (800000000 - 1)
      Computing sum 900000000 + ... + (1000000000 - 1)
      Computing sum 400000000 + ... + (500000000 - 1)
      Joined with thread 0
      Joined with thread 1
      Joined with thread 2
      Joined with thread 3
      Joined with thread 4
      Joined with thread 5
      Joined with thread 6
      Joined with thread 7
      Joined with thread 8
      Joined with thread 9
      Summing worker totals
      total=499999999500000000
      Elapsed time: 444 ms
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-04-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-26
        • 1970-01-01
        相关资源
        最近更新 更多