一个合适的基准测试?

3

我想要测量两个不同程序执行一项任务所需的时间。其中一个程序使用了线程,另一个则没有。任务是计数到2000000。

具有线程的类:

public class Main {
    private int res1 = 0;
    private int res2 = 0;

    public static void main(String[] args) {
        Main m = new Main();

        long startTime = System.nanoTime();
        m.func();
        long endTime = System.nanoTime();

        long duration = endTime - startTime;
        System.out.println("duration: " + duration);
    }

    public void func() {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    res1++;
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 1000000; i < 2000000; i++) {
                    res2++;
                }
            }
        });

        t1.start();
        t2.start();

        System.out.println(res1 + res2);
    }
}

不涉及线程的类:

public class Main {

    private int res = 0;

    public static void main(String[] args) {
        Main m = new Main();

        long startTime = System.nanoTime();
        m.func();
        long endTime = System.nanoTime();

        long duration = endTime - startTime;
        System.out.println("duration: " + duration);

    }

    public void func() {

        for (int i = 0; i < 2000000; i++) {
            res++;
        }
        System.out.println(res);
    }
}

经过10次测量,平均结果(以纳秒为单位)如下:

With threads:    1952358
Without threads: 7941479

我做得对吗?
使用2个线程为什么速度会快4倍,而不仅仅是2倍?


1
你是否运行了多次以确保结果相似? - hmatar
你的处理器有多少个核心? - syb0rg
相关:https://wikis.oracle.com/display/HotSpotInternals/MicroBenchmarks - Paul Bellora
3
您希望不使用.join()方法来让这两个线程同步吗? - fge
2
在您的第一个示例中,两个线程可能在返回结果之前尚未完成... - home
4个回答

8

在这些代码行中

    t1.start();
    t2.start();

您正在启动线程执行,但在进行时间测量之前实际上没有等待它们完成。为了等待线程完成,请调用

   t1.join();
   t2.join();

join 方法会一直阻塞,直到线程执行完成。然后测量执行时间。


5
在并行版本中,您正在测量主线程创建另外两个线程的数量。您没有测量它们的执行时间。这就是为什么您会得到超线性加速的原因。为了包括它们的执行时间,您必须将它们与主线程合并。
t2.start(); 之后添加以下行:
     t1.join();  // wait until thread t1 terminates
     t2.join(); // wait until thread t2 terminates

2

多线程版本更快的主要原因是你不需要等待循环完成,只需等待线程开始。

在start()之后需要添加:

    t1.join();
    t2.join();

一旦你这样做了,你会注意到启动线程需要很长时间,而且速度要慢得多。如果你让测试时间延长100倍,那么启动线程的成本就不那么重要了。

单线程示例需要更长时间才能被适当地JIT编译。你需要确保至少运行测试2秒钟,并重复运行。

我的多线程版本是

public class Main {
    private long res1 = 0;
    public long p0, p1, p2, p3, p4, p5, p6, p7;
    private long res2 = 0;

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();

        for (int i = 0; i < 10; i++) {
            long startTime = System.nanoTime();
            m.func();
            long endTime = System.nanoTime();

            long duration = endTime - startTime;
            System.out.println("duration: " + duration);
        }
        assert m.p0 + m.p1 + m.p2 + m.p3 + m.p4 + m.p5 + m.p6 + m.p7 == 0;
    }

    public void func() throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000000; i++) {
                    res1++;
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1000000000; i < 2000000000; i++) {
                    res2++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(res1 + res2);
    }
}

对于多线程测试,将以下内容打印出来。

2000000000
duration: 179014396
4000000000
duration: 148814805
.. deleted ..
18000000000
duration: 61767861
20000000000
duration: 72396259

对于单线程版本,我注释掉一个线程并得到以下结果。
2000000000
duration: 266228421
4000000000
duration: 255203050
... deleted ...
18000000000
duration: 125434383
20000000000
duration: 125230354

如预期的那样,当运行足够长时间时,两个线程几乎比一个线程快一倍。

简而言之,

  • 如果您不等待那些操作完成(例如异步日志记录和消息传递),多线程代码可以使当前线程的延迟更小。

  • 除非您有重要的 CPU 绑定任务要执行(或者可以进行并发 IO),否则单线程编码可以比多线程编码快得多(且更简单)。

  • 在同一 JVM 中重复运行测试可能会产生不同的结果。


1

在Java基准测试时,有一些技巧需要记住。

首先,当基准测试任何东西时都是相同的:一个运行可能比另一个运行慢,没有任何有意义的原因。为了避免这种情况,运行多次并取平均值(我指的是很多次)。

第二个技巧可能不是Java独有的,但可能会让人感到惊讶:Java虚拟机可能需要时间来“热身”-如果您运行代码100次,则编译后的代码可以根据哪些代码路径极其常见而发生更改。为了解决这个问题,在开始统计之前多次运行代码。

热身需要多长时间取决于您的JVM设置-我无法立即想起来。

当然,这与其他答案指出的问题完全不同,您实际上没有测量线程化程序。

编辑:还要注意的另一件事是编译器意识到任何特定变量/循环/整个程序都是完全无意义的。在这些情况下,它很可能会将其完全删除-您可能会发现需要使用res1res2,否则您的循环可能会从编译后的代码中完全删除。

编辑:刚意识到您实际上确实使用了所有计数变量-不过这仍然是一个有用的知识点,所以我会保留它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接