Доступ к памяти несколькими потоками

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

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

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

PS Если потоки выполняли простой математический расчет, то для 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 потоков может захватывать только свой процессор.

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

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

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

6 ответов

Решение

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

Просто проверка работоспособности, вы также используете параллельный коллектор? -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
Другие вопросы по тегам