Вопрос

Я пишу многопоточное java-приложение, которое работает на процессоре Nehalem.Однако у меня есть проблема в том, что, начиная с 4 потоков, я почти не вижу ускорения в своем приложении.

Я провел несколько простых тестов.Я создал поток, который просто выделяет большой массив и предоставляет доступ к случайным записям в массиве.Поэтому, когда я запускаю количество потоков, время выполнения не должно меняться (при условии, что я не превышаю количество доступных ядер процессора).Но что я заметил, так это то, что запуск 1 или 2 потоков занимает почти столько же времени, но запуск 4 или 8 потоков происходит значительно медленнее.Поэтому, прежде чем пытаться таким образом решить проблему алгоритмизации и синхронизации в моем приложении, я хочу выяснить, какого максимально возможного распараллеливания я могу достичь.

Я использовал -XX:+UseNUMA Опция JVM, поэтому массивы должны быть размещены в памяти рядом с соответствующими потоками.

P.S.Если потоки выполняли простые математические вычисления, то для 4 и даже 8 потоков не было потери времени, поэтому я пришел к выводу, что когда потоки обращаются к памяти, у меня возникают некоторые проблемы.

Любая помощь или идеи приветствуются, спасибо.


Редактировать

Спасибо вам всем за ответы.Я вижу, что недостаточно хорошо объяснился.

Прежде чем попытаться устранить проблемы с синхронизацией в моем приложении, я провел простой тест, который проверял наилучшее возможное распараллеливание, которое могло быть достигнуто.Код выглядит следующим образом:

public class TestMultiThreadingArrayAccess {
    private final static int arrSize = 40000000;

    private class SimpleLoop extends Thread {
        public void run() {
            int array[] = new int[arrSize];
            for (long i = 0; i < arrSize * 10; i++) {
                array[(int) ((i * i) % arrSize)]++; // randomize a bit the access to the array
            }
            long sum = 0;
            for (int i = 0; i < arrSize; i++)
                sum += array[i];
        }
    }

    public static void main(String[] args) {
        TestMultiThreadingArrayAccess test = new TestMultiThreadingArrayAccess();
        for (int threadsNumber : new int[] { 1, 2, 4, 8 }) {
            Statistics timer = new Statistics("Executing " + threadsNumber+ " threads"); // Statistics is a simple helper class that measures the times
            timer.start();
            test.doTest(threadsNumber);
            timer.stop();
            System.out.println(timer.toString());
        }
    }

    public void doTest(int threadsNumber) {
        Thread threads[] = new Thread[threadsNumber];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new SimpleLoop();
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++)
            try {
                threads[i].join();
            } catch (InterruptedException e) {
            };
    }
}

Итак, как вы видите, в этом минитесте вообще нет синхронизации, а также выделение массива находится внутри потока, поэтому его следует поместить в тот фрагмент памяти, к которому можно быстро получить доступ.Также в этом коде нет проблем с памятью.Тем не менее, для 4 потоков время выполнения сокращается на 30%, а для 8 потоков оно выполняется в два раза медленнее.Как видно из кода, я просто жду, пока все потоки завершат свою работу, и поскольку их работа независима, количество потоков не должно влиять на общее время выполнения.

На компьютере установлены 2 четырехъядерных гиперпоточных процессора Nehalem (всего 16 процессоров), так что с 8 потоками каждый из них мог использовать исключительно свой процессор.

Когда я попытался запустить этот тест с меньшим массивом (20 тыс. записей), сокращение времени выполнения 4 потоков составило 7%, а 8 потоков - 14%, что вполне удовлетворительно.Но когда я пытаюсь использовать произвольный доступ к большому массиву (40 млн записей), время выполнения резко увеличивается, поэтому я думаю, что проблема заключается в том, что большие куски памяти (потому что они не помещаются в кэш-память?) доступны неэффективным способом.

Есть ли какие-нибудь идеи, как это исправить?

Надеюсь, это лучше прояснит вопрос, еще раз спасибо.

Это было полезно?

Решение

Узким местом в тесте является пропускная способность процессора по отношению к памяти.Даже когда локальная память доступна, она будет совместно использоваться некоторым количеством потоков.(Память локальна для узла, а не для конкретного ядра.) Однажды процессор может легко превысить доступную пропускную способность для простого цикла, подобного вашему приведенному выше тесту, и поэтому увеличение потоков в таком тесте не улучшит производительность, а может ухудшить производительность из-за ухудшения согласованности кэша.

Просто проверка на вменяемость, вы тоже используете параллельный коллектор? -XX:+UseParallelGC.UseNUMA вступает в силу только тогда.

Другие советы

Не зная, что именно вы делаете и какую проблему пытаетесь решить.Похоже, у вас сильная синхронизация вокруг вашего кода, поскольку это может быть основной причиной недостаточной масштабируемости.Чрезмерная синхронизация приводит к замедлению любого ускорения, как только это делает ваше приложение почти последовательным.Итак, мое предложение вам - проверить вашу реализацию и попытаться разобраться в этом.

Добавить.

После того, как вы добавите свою реализацию того, что вы делаете.Снижение производительности может быть объяснено большим доступом к памяти.Как только вы запускаете все свои потоки, и им необходимо получить доступ к контроллеру памяти для некэшированных данных, поскольку они работают на разных процессорах, контроллер памяти не позволяет процессорам выполнять это одновременно, что означает синхронизацию на аппаратном уровне при каждом пропуске кэша.В вашем случае это почти равносильно тому, как если бы вы запускали 10 разных независимых программ.Я предполагаю, что если вы запустите, например, 10 копий вашего веб-браузера (вы можете заменить 10 любым большим числом), вы увидите тот же эффект, но это не означает, что реализация браузера неэффективна, вы просто создаете огромную нагрузку на память компьютера.

Как отмечает Артем, вполне возможно, что у вас ненужная синхронизация.Но я бы начал с установления фактов.Ваше приложение ДЕЙСТВИТЕЛЬНО работает медленнее, как вы описываете?

Вот содержательная статья на эту тему: http://codeidol.com/java/java-concurrency/Testing-Concurrent-Programs/Avoiding-Performance-Testing-Pitfalls/

На самом деле довольно сложно написать полезные микро-бенчмарки, особенно когда вы имеете дело с параллельным кодом.Например, у вас может быть "Устранение мертвого кода", при котором компилятор оптимизирует код, который, по вашему мнению, выполняется.Также трудно угадать, когда запускается сборка мусора.Оптимизация времени выполнения Hotspot также усложняет измерение.В случае потоков вам необходимо учитывать время, затраченное на их создание.Таким образом, вам, возможно, придется использовать CyclicBarrier`и т.д.чтобы иметь точные измерения.Что-то в этом роде..

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

Есть две очевидные потенциальные проблемы, которые приходят на ум.

  • Использование большего количества потоков выделяет больше массивов, которые расширяют кэш.Доступ к основной памяти или к более низким уровням кэша происходит намного медленнее.
  • Если вы используете тот же источник экземпляра генератора случайных чисел, то потоки будут бороться за доступ к нему.Возможно, это не полная синхронизация, а вместо этого барьеры памяти с алгоритмом без блокировки.Как правило, алгоритмы без блокировок, хотя в целом и быстры, становятся намного медленнее при высокой конкуренции.

Помимо проблем с параллелизмом, наиболее вероятной причиной вашего замедления является конфликт в кэше памяти.

Если все потоки обращаются к одному и тому же фрагменту памяти, скорее всего, он находится в кэше памяти других процессоров, когда вы захотите получить к нему доступ.

Если хранилище доступно "только для чтения", вы могли бы предоставить каждому потоку свою собственную копию, что позволило бы JVM и процессору оптимизировать доступ к памяти.

Я изменил ваш тест, руководствуясь рекомендациями из статьи, которую я опубликовал.На моем 2-ядерном компьютере (это все, что у меня есть прямо сейчас) результат кажется разумным (обратите внимание, что я провел 2 теста для каждого номера потока):

Может быть, вы можете попробовать это?(Пожалуйста, обратите внимание, что мне пришлось немного изменить ваш тест (см. Комментарий), потому что его запуск на моем бедном оборудовании занял очень много времени)

Также обратите внимание, что я запускаю этот тест, используя -server вариант.

Test with threadNum 1 took 2095717473 ns
Test with threadNum 1 took 2121744523 ns
Test with threadNum 2 took 2489853040 ns
Test with threadNum 2 took 2465152974 ns
Test with threadNum 4 took 5044335803 ns
Test with threadNum 4 took 5041235688 ns
Test with threadNum 8 took 10279012556 ns
Test with threadNum 8 took 10347970483 ns

код:

import java.util.concurrent.*;

public class Test{
    private final static int arrSize = 20000000;

    public static void main(String[] args) throws Exception {
        int[] nums = {1,1,2,2,4,4,8,8};//allow hotspot optimization
        for (int threadNum : nums) {
            final CyclicBarrier gate = new CyclicBarrier(threadNum+1);
            final CountDownLatch latch = new CountDownLatch(threadNum);
            ExecutorService exec = Executors.newFixedThreadPool(threadNum);
            for(int i=0; i<threadNum; i++){
                Runnable test = 
                  new Runnable(){
                     public void run() {
                         try{
                             gate.await();
                         }catch(Exception e){
                             throw new RuntimeException(e);
                         }
                         int array[] = new int[arrSize];
                         //arrSize * 10 took very long to run so made it
                         // just arrSize.
                         for (long i = 0; i < arrSize; i++) {
                             array[(int) ((i * i) % arrSize)]++;
                         }//for
                         long sum = 0;
                         for (int i = 0; i < arrSize; i++){
                              sum += array[i]; 
                         }
                         if(new Object().hashCode()==sum){
                              System.out.println("oh");
                         }//if
                         latch.countDown();
                      }//run
                   };//test
                exec.execute(test);
             }//for
             gate.await();
             long start = System.nanoTime();
             latch.await();
             long finish = System.nanoTime();
             System.out.println("Test with threadNum " +
                 threadNum +" took " + (finish-start) + " ns ");
             exec.shutdown();
             exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);           
        }//for
    }//main

}//Test
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top