Бенчмарк в многопоточной среде

Я изучал многопоточность и обнаружил замедление Object.hashCode в многопоточной среде, поскольку для вычисления хэш-кода по умолчанию, выполняющего 4 threads против 1 thread, для того же количества объектов требуется в два раза больше времени.

Но, насколько я понимаю, это должно занять аналогичное количество времени, делая это параллельно.

Вы можете изменить количество потоков. Каждый поток выполняет одинаковый объем работы, поэтому вы надеетесь, что выполнение 4 потоков на моей четырехъядерной машине может занять примерно столько же времени, сколько выполнение одного потока.

Я вижу примерно 2,3 секунды при 4-кратном увеличении и 0,9 с при 1-кратном увеличении.

Есть ли пробел в моем понимании, пожалуйста, помогите мне понять это поведение.

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

person Sachin Sachdeva    schedule 16.12.2015    source источник
comment
Пожалуйста, поделитесь внесенными изменениями между 1 и 4 потоками   -  person Jan    schedule 16.12.2015
comment
Измерение времени здесь не обязательно много говорит. См. stackoverflow.com/questions/504103/   -  person Marco13    schedule 16.12.2015
comment
Вы, вероятно, измеряете не то, что нужно: GC, создание исполнителей и его потоков, координацию потоков, создание экземпляров объектов, выделение памяти и т. д. и т. д. В любом случае, бенчмарк довольно бесполезен, так как вы не сможете в любом случае измените что-либо в реализации объекта hashCode().   -  person JB Nizet    schedule 16.12.2015
comment
Вы не измеряете hashCode(), вы измеряете создание экземпляров 20 миллионов объектов при однопоточном и 80 миллионов объектов при запуске 4 потоков. Переместите новую логику Object() из цикла for в свой Callable, тогда вы будете измерять hashCode()   -  person Palamino    schedule 16.12.2015
comment
Кроме того, hashCode for Object на самом деле реализуется с собственным вызовом для конкретной платформы, поэтому вы, скорее всего, не обнаружите здесь никаких проблем с производительностью.   -  person Davio    schedule 16.12.2015


Ответы (3)


Я создал простой тест 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() в своем тесте.

person Svetlin Zarev    schedule 16.12.2015
comment
Не могли бы вы рассказать об инструменте, который вы использовали для бенчмаркинга? - person Sachin Sachdeva; 27.12.2015

См. комментарий Паламино:

Вы не измеряете hashCode(), вы измеряете создание экземпляров 20 миллионов объектов при однопоточном и 80 миллионов объектов при запуске 4 потоков. Переместите новую логику Object() из цикла for в Callable, тогда вы будете измерять hashCode() – Palamino

person Tom Cools    schedule 16.12.2015
comment
Он сказал, что вы можете изменить количество потоков, чтобы наблюдать за проблемой, которую он описал. - person Marco13; 16.12.2015
comment
Я переместил его тот же результат.. :( - person Sachin Sachdeva; 16.12.2015

Две проблемы, которые я вижу с кодом:

  1. Размер массива allWork[] равен ITERATIONS.
  2. И во время итерации в методе call() убедитесь, что каждый поток получает свою долю нагрузки. ИТЕРАЦИЙ/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();
    }
 }

 }
person Madhusudana Reddy Sunnapu    schedule 16.12.2015
comment
в методе run()/call() вы все еще выделяете объекты, поэтому вы измеряете хэш-код плюс скорость выделения. Ваш ответ ошибочен. - person Svetlin Zarev; 16.12.2015