多线程环境下的基准测试

7
我正在学习多线程并发,发现在多线程环境下 Object.hashCode 的速度会变慢,同样数量的对象进行计算时,运行 4 个 threads 相对于 1 个 thread,所需时间要多出一倍以上。
但根据我的理解,这应该并行处理需要同样的时间。
你可以更改线程数。每个线程要做相同的工作量,因此在我的四核机器上,希望运行 4 个线程需要和单线程运行花费相同的时间。
我发现运行 4 个线程需要大约 2.3 秒,而单线程只需要 0.9 秒。
请问是否有什么误解?请帮助我理解这种行为。
public class ObjectHashCodePerformance {

private static final int THREAD_COUNT = 4;
private static final int ITERATIONS = 20000000;

public static void main(final String[] args) throws Exception {
    long start = System.currentTimeMillis();
    new ObjectHashCodePerformance().run();
    System.err.println(System.currentTimeMillis() - start);
 }

private final ExecutorService _sevice =   Executors.newFixedThreadPool(THREAD_COUNT,
        new ThreadFactory() {
            private final ThreadFactory _delegate =   Executors.defaultThreadFactory();

            @Override
            public Thread newThread(final Runnable r) {
                Thread thread = _delegate.newThread(r);
                thread.setDaemon(true);
                return thread;
            }
        });

    private void run() throws Exception {
    Callable<Void> work = new java.util.concurrent.Callable<Void>() {
        @Override
        public Void call() throws Exception {
            for (int i = 0; i < ITERATIONS; i++) {
                Object object = new Object();
                object.hashCode();
            }
            return null;
        }
    };
    @SuppressWarnings("unchecked")
    Callable<Void>[] allWork = new Callable[THREAD_COUNT];
    Arrays.fill(allWork, work);
    List<Future<Void>> futures = _sevice.invokeAll(Arrays.asList(allWork));
    for (Future<Void> future : futures) {
        future.get();
    }
 }

 }

对于线程数为4的情况,输出结果为:
~2.3 seconds

线程数为1时,输出结果为

~.9 seconds

请分享您在1到4个线程之间所做的更改。 - Jan
时间测量在这里并不一定告诉你很多。请参见https://dev59.com/hHRB5IYBdhLWcg3wz6UK - Marco13
1
你可能没有测量正确的事情:GC、执行器及其线程的创建、线程协调、对象实例化、内存分配等等。无论如何,这个基准测试是相当无用的,因为你无法改变Object的hashCode()实现。 - JB Nizet
3
你所测量的不是hashCode()方法,而是在单线程时实例化2000万个对象,4线程时实例化8000万个对象。将new Object()逻辑移到Callable的for循环之外,然后你就可以测量hashCode()方法了。 - Palamino
此外,Object的hashCode实际上是通过本地平台特定的调用来实现的,因此您可能不会在那里发现任何性能问题。 - Davio
3个回答

7
我已经创建了一个简单的JMH基准测试来测试各种情况:
@Fork(1)
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10)
@Warmup(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
public class HashCodeBenchmark {
    private final Object object = new Object();

    @Benchmark
    @Threads(1)
    public void singleThread(Blackhole blackhole){
        blackhole.consume(object.hashCode());
    }

    @Benchmark
    @Threads(2)
    public void twoThreads(Blackhole blackhole){
        blackhole.consume(object.hashCode());
    }

    @Benchmark
    @Threads(4)
    public void fourThreads(Blackhole blackhole){
        blackhole.consume(object.hashCode());
    }

    @Benchmark
    @Threads(8)
    public void eightThreads(Blackhole blackhole){
        blackhole.consume(object.hashCode());
    }
}

以下为结果:
Benchmark                       Mode  Cnt  Score   Error  Units
HashCodeBenchmark.eightThreads  avgt   10  5.710 ± 0.087  ns/op
HashCodeBenchmark.fourThreads   avgt   10  3.603 ± 0.169  ns/op
HashCodeBenchmark.singleThread  avgt   10  3.063 ± 0.011  ns/op
HashCodeBenchmark.twoThreads    avgt   10  3.067 ± 0.034  ns/op

因此,只要线程数不超过核心数,每个哈希码的时间保持不变。

PS:正如@Tom Cools所评论的那样-您在测试中测量的是分配速度而不是hashCode()速度。


请问您可以告诉我您用于基准测试的工具吗? - T-Bag

1

看看Palamino的评论:

你不是在测量hashCode(),而是在单线程时实例化2000万个对象,在运行4个线程时实例化8000万个对象。将new Object()逻辑移出Callable中的for循环,那么您将会测量hashCode() - Palamino


2
他说你可以更改线程数来观察他描述的问题。 - Marco13
我把它移出去了,结果还是一样..:( - T-Bag

0

我看到代码中存在两个问题:

  1. allWork [] 数组的大小等于 ITERATIONS。
  2. 在迭代过程中,在 call() 方法中确保每个线程都能得到其负载份额。ITERATIONS/THREAD_COUNT。

下面是您可以尝试的修改版本:

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;

 public class ObjectHashCodePerformance {

private static final int THREAD_COUNT = 1;
private static final int ITERATIONS = 20000;
private final Object object = new Object();

public static void main(final String[] args) throws Exception {
    long start = System.currentTimeMillis();
    new ObjectHashCodePerformance().run();
    System.err.println(System.currentTimeMillis() - start);
 }

private final ExecutorService _sevice =   Executors.newFixedThreadPool(THREAD_COUNT,
        new ThreadFactory() {
            private final ThreadFactory _delegate =   Executors.defaultThreadFactory();

            @Override
            public Thread newThread(final Runnable r) {
                Thread thread = _delegate.newThread(r);
                thread.setDaemon(true);
                return thread;
            }
        });

    private void run() throws Exception {
    Callable<Void> work = new java.util.concurrent.Callable<Void>() {
        @Override
        public Void call() throws Exception {
            for (int i = 0; i < ITERATIONS/THREAD_COUNT; i++) {
                object.hashCode();
            }
            return null;
        }
    };
    @SuppressWarnings("unchecked")
    Callable<Void>[] allWork = new Callable[ITERATIONS];
    Arrays.fill(allWork, work);
    List<Future<Void>> futures = _sevice.invokeAll(Arrays.asList(allWork));
    System.out.println("Futures size : " + futures.size());
    for (Future<Void> future : futures) {
        future.get();
    }
 }

 }

1
run()/call()方法中,您仍在分配对象 - 因此您正在测量哈希码加上分配速度。您的答案是有缺陷的。 - Svetlin Zarev

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