1

У меня есть вопрос о параллельном программном обеспечении java.Java Concurrent - нет speedUp получил алгоритм LU - Ложное разделение?

Основным алгоритмом моего приложения является фактор матрицы A в LU матриц: A = LU. Метод разложения, вставленный здесь, приводит к уменьшению Гаусса-Иордана. Программное обеспечение предназначено для работы на квадратной матрице с позицией A[0][0] != 0.

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

Я попытался сделать этот алгоритм параллельно с использованием барьера (для ждать актуализацию каждой строки и приращения «rigaCorrente» значения), но я не получить реальный SpeedUp, даже если в параллельной версии моих процессоры (2,4 ГГц Core 2 Duo P8600) работают на 80% от общего количества (вместо 40-50% серийного).

Мой страх в том, что я столкнулся с ложным обменом ситуация. Является ли проблема связана с ложным обменом или с другими вопросами? Я знаю, что JVM выполняет хорошие оптимизации, но по-прежнему использует половину мощности процессора. Я не думаю, что невозможно улучшить алгоритм!

Мой серийный код:

public void decompose(){ 
    int n = A.length; 
    for(int k=0; k<n-1;k++) { 
     for(int i=k+1; i<n; i++) { 
      A[i][k] = A[i][k]/A[k][k]; 
      for(int j=k+1; j<n; j++) { 
       A[i][j] = A[i][j] - A[i][k] * A[k][j]; 
      } 
     } 
    } 
    decomposed = true; 
} 

Мои параллельно код:

public class Manager { 
private double [][] A; 

private Semaphore main = new Semaphore(0); 
private CyclicBarrier barriera; 

private AtomicInteger index; 
private int rigaCorrente = 0; 
private boolean stop = false; 

public Manager(final double A[][], final int numThr){ 
    this.A = A; 
    this.index = new AtomicInteger(1); 

    barriera = new CyclicBarrier(numThr, new Runnable(){ 

     @Override 
     public void run() { 

     rigaCorrente++; 
     index = new AtomicInteger(rigaCorrente+1); 

    if(rigaCorrente == A.length - 1){ 
      setStop(true); 
      main.release(); 
     } 
    } 

    }); 
} 

Класс резьбы:

public class Deco implements Runnable { 

private Manager manager; 

public Deco(Manager manager){ 
    this.manager = manager; 
} 

@Override 
public void run() { 
    double [][] A = manager.getA(); 

while(manager.getStop() == false){ 
    int i; 
    while((i = (manager.getIndex().getAndIncrement())) < (A.length)){ 
    double pivot = A[i][manager.getRigaCorrente()]/A[manager.getRigaCorrente()]      [manager.getRigaCorrente()]; 

     for(int k = manager.getRigaCorrente(); k<A.length; k++) 
      A[i][k] = A[i][k] - (pivot*A[manager.getRigaCorrente()][k]); 

     A[i][manager.getRigaCorrente()] = pivot; 
} 

     manager.acquireBarriera(); 

    }// Stop 

} 

    } 

Главное для параллельного кода:

package luConcurrent.test; 
    import java.util.Arrays; 
    import java.util.Scanner; 

    import lu.DecompositionLU; 
    import lu.IO; 

    public class Starter { 
private static IO io; 
private static DecompositionLU dec; 
public static void main(String[] args) throws Exception { 
    io = new IO("//Users//black//Desktop//serie//2500.txt"); 
    int numThr = 2; 

    double [][] A = io.readMatrixFromInputFile(); 
    double [] b = io.readArrayFromInputFile(); 
    double [] x; 

    dec = new DecompositionLU(A); 

    System.out.println("A.length: "+A.length); 

    Manager manager = new Manager(A,numThr); 
    Thread[] pool = new Thread[numThr]; 

    for(int i=0; i<pool.length; i++){ 
     pool[i] = new Thread(new Deco(manager)); 
    } 

    long inizio = System.nanoTime(); 

    for(int i = 0; i<pool.length; i++){ 
     pool[i].start(); 
    } 

    manager.getMain().acquire(); 

    dec.ProgresiveSustitution(b); 
    x = dec.RegresiveSustitution(b); 

    long fine = System.nanoTime()-inizio; 

    System.out.println("Solution is: "+Arrays.toString(x)); 
    Scanner sc = new Scanner(System.in); 
    sc.nextLine(); 
    System.out.println("Tempo: "+fine); 
    sc.close(); 

} 
    } 

Результаты:

1000x1000 Serial: 1154679000 nanoSec 
    1000x1000 Parallel 2 Threads: 1135663000 nanoSec 

    1750x1750 Serial: 7502559000 nanoSec 
    1750x1750 Parallel 2 Threads: 6667129000 nanoSec 

    4000x4000 Serial: 89851311000 nanoSec 
    4000x4000 Parallel 2 Threads: 84348616000 nanoSec 
+0

можете ли вы разместить больше кода и, возможно, пропустить несколько пустых строк? –

+0

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

+0

Я не знаю, есть ли у меня большое количество промахов, как это проверить? – BlacK

ответ

2

Я бы не стал делать вывод о том, что происходит ложное совместное использование. Параллельная версия алгоритма добавляет кучу накладных расходов, что, вероятно, отвечает за снижение производительности ниже того, что вы ожидаете.

Последовательная версия имеет три вложенные петли: внешняя петля над k, средняя петля над i и внутренний цикл над j. Все, что он делает - это доступ к массиву и арифметика, поэтому это должно быть довольно быстро.

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

Средний цикл превышает index, который является AtomicInteger, вызванным вызовом метода getIndex. Вызов метода добавляет служебные данные, а getAndIncrement добавляет некоторые противоречия между потоками.

Внутренний контур (смешано над k вместо j, как в серийной версии) имеет метод вызова getRigaCorrente внутри него. Иногда - но только иногда - JVM может встроить вызовы методов. Я не вижу реализации getRigaCorrente здесь, но так как rigaCorrente является частной переменной экземпляра manager, которая не является изменчивой, и она считывается и записывается несколькими потоками, возможно, этот метод синхронизирован. Это добавит еще больше накладных расходов.

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

+0

rigaCorrente только краснеет по потокам и написан Runnable внутри CyclicBarrier ** numThr ** threads call ** manager.acquireBarriera() **. Для работы с разбиением я пробовал этот подход, создавая потоки с двумя полями «start» и «end». Реальная проблема заключается в том, что если матрица (допустим, длина 100) разлагается от 1 до 50, все нити, у которых есть «конечное» поле <= 50, не работают. Насколько мне известно, мне нужно каким-то образом передать поле «rigaCorrente». – BlacK

+0

Несмотря на то, что rigaCorrente может быть прочитан и написан только потоками в хорошо понятные времена, из-за модели памяти Java, один ** должен либо заблокировать все обращения к нему (чтение и запись), либо сделать его изменчивым. Полный ответ слишком подробный для комментария, но один из способов подумать о том, что записи могут быть кэшированы и не обязательно будут видимы для других потоков, если не используется блокировка или нестабильность. –

+0

Что касается разделения работы между потоками, это должно выполняться в зависимости от конкретного приложения. Я мало знаю о LU-декомпозиции, но может быть, что «очевидный» способ расщепления матрицы пополам не работает, потому что, например, работа ниже в матрице зависит от выполненной над ней работы в матрица. Наличие проблем с файлами rigaCorrente может предотвратить такие проблемы, за счет эффективного одновременного запуска нескольких потоков, что приводит к ускорению. Бьюсь об заклад, есть аналитические статьи о том, как распараллелить LU-декомпозицию. –

1

полностью переписанный ОТВЕТ:

Вот мое решение с помощью вилки/Join рамки:

public void decompose() 
{ 
    final Semaphore semaphore = new Semaphore(0); 

    class Decompose extends RecursiveAction { 
     private final int k; 

     Decompose(int k) { 
      this.k = k; 
     } 

     protected void compute() { 
      final int n = A.length; 
      for (int i = k + 1; i < n; i++) { 
       A[i][k] = A[i][k]/A[k][k]; 
       for (int j = k + 1; j < n; j++) { 
        A[i][j] = A[i][j] - A[i][k] * A[k][j]; 
       } 
      } 

      semaphore.release(); 
     } 
    }; 

    ForkJoinPool mainPool = new ForkJoinPool(); 
    for (int k = 0; k < A.length - 1; k++) { 
     mainPool.execute(new Decompose(k)); 
    } 
    semaphore.acquireUninterruptibly(A.length - 1); 
} 

В моем случае, это число Я получил для матрицы 1000x1000:

1000x1000 Serial : 234351000 nanoSec 
1000x1000 Fork/Join: 61580000 nanoSec 

Это на iMac i7 2600.

+0

ForkJoin не волшебным образом делает вещи быстрее, а скорее наоборот. См. [Мой анализ] (http://stackoverflow.com/questions/20288379/analysis-performance-of-forkjoinpool) на SO. – TwoThe

+0

Я не сказал, что это ускорится.Я сказал, что это, вероятно, будет менее сложным по коду. BTW, [this] (http://coopsoft.com/ar/CalamityArticle.html) - это гораздо лучший анализ всего, что не так с фреймворком Fork/Join в Java. – brettw

+0

Спасибо за очень интересную ссылку. Но тогда: почему вы по-прежнему рекомендуете его? ;) – TwoThe

-1

Есть несколько вещей, которые не соответствуют вашему измерению времени. Прежде всего, вы включаете вызов start() в свои измерения, что было бы хорошо, если вы хотите узнать время, если вы не будете повторно использовать эти потоки, но тогда вы также должны включить в себя накладные расходы на создание. Если вы планируете повторно использовать потоки, вы должны оставить стартовый вызов вне измерения времени, так как это намного больше, чем простое значение boolean для true.

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

Тогда звонок manager.getRigaCorrente() выглядит очень подозрительно. В вашем классе Manager это не определено, но присутствует int с тем же именем. Независимо от того, что вы получите оттуда, вы, скорее всего, либо нарушите правила потоковой передачи, либо, если вы правильно синхронизируете его, сильно замедлит работу, так что, похоже, также существует алгоритмическая проблема. Трудно сказать, не используете ли вы настоящий код.

Смежные вопросы