Nano Hash - криптовалюты, майнинг, программирование

Как мне запустить что-то параллельное в Java?

Я пытаюсь напечатать все возможные комбинации в пределах диапазона. Например, если мой lowerBound равен 3, а мой max равен 5, мне нужны следующие комбинации: (5,4 - 5,3 - 4,3). Я реализовал это с помощью функции helper(), приведенной ниже.

Конечно, если мой макс очень большой, то это очень много комбинаций, и это займет много времени. Вот почему я пытаюсь реализовать ForkJoinPool, чтобы задачи выполнялись параллельно. Для этого я создаю новый файл ForkJoinPool. Затем я перебираю все возможные значения r (где r — количество чисел в комбинации, в приведенном выше примере r=3). Для каждого значения r я создаю новый HelperCalculator, который расширяет RecursiveTask<Void>. Там я рекурсивно вызываю функцию helper(). Каждый раз, когда я вызываю это, я создаю новый HelperCalculator и использую для этого .fork().

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

Очевидно, я что-то неправильно понимаю в ForkJoinPool, но я больше не могу понять, что после нескольких дней попыток.

Моя основная функция:

            ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
            for (int r = 1; r < 25; r++) {
                int lowerBound = 7;
                int[] data = new int[r];
                int max = 25;
                calculator = new HelperCalculator(data, 0, max, 0, s, n, lowerBound);
                pool.execute(calculator);
                calculator.join();
            }
            pool.shutdown();

Класс HelperCalculator:

    protected Void compute() {
        helper(data, end, start, index, s, lowerBound);
        return null;
    }

    //Generate all possible combinations
    public void helper(int[] data , int end, int start, int index,int s, int lowerBound) {
        //If the array is filled, print it
        if (index == data.length) {
                System.out.println(Arrays.toString(data));
        } else if (start >= end) {
            data[index] = start;
            if(data[0] >= lowerBound) {
                HelperCalculator calculator = new HelperCalculator(data,end, start-1, index+1, s, n, lowerBound);
                calculator.fork();
                calculators.add(calculator);
                HelperCalculator calculator2 = new HelperCalculator(data, end, start-1, index, s, n, lowerBound);
                calculator2.fork();
                calculators.add(calculator2);
            }
        }

Как сделать так, чтобы каждое HelperCalculator запускалось параллельно, чтобы одновременно выполнялось 23 запуска с использованием ForkJoinPool? Или я должен использовать другое решение?

Я пробовал вызывать join() и isDone() в списке calculators, но тогда он не ждал, пока он правильно завершится, и программа просто закрывалась.

Поскольку кто-то не понимает алгоритма, вот он:

    public static void main(String[] args) {
            for(int r = 3; r > 0; r--) {
                int[] data = new int[r];
                helper(data, 0, 2, 0);
            }
    }

    public static void helper(int[] data , int end, int start, int index) {
        if (index == data.length) {
            System.out.println(Arrays.toString(data));
        } else if (start >= end) {
            data[index] = start;
                helper(data, end, start - 1, index + 1);
                helper(data, end, start - 1, index);
            }
        }
    }

Результат этого:

[2, 1, 0]
[2, 1]
[2, 0]
[1, 0]
[2]
[1]
[0]

  • Вызов join() не может помочь, когда действие, которое должно произойти после этого, не помещено после него. Вот что происходит, когда вы смешиваете код, который должен выполнять вычисления, с кодом, который должен обрабатывать (печатать) результат. 08.04.2020
  • Кроме того, единственный код, который действительно что-то делает, это data[index] = start; Это ни то, ни другое, осмысленное вычисление и не выгода от параллельной обработки. 08.04.2020
  • Итак, вы говорите, что в этом алгоритме нет возможности генерировать комбинации параллельно, верно? 08.04.2020
  • Нет, мне не хватает реального алгоритма в вашем коде. Возможно, вам лучше начать с реализации работающего последовательного алгоритма, прежде чем обсуждать, как его распараллелить. 08.04.2020
  • Я думаю, что неправильно понимаю, что вы имеете в виду, поэтому здесь я попытаюсь объяснить, что я делаю: ``` public void helper(int[] data , int end, int start, int index) { if (index == data.length) { System.out.println (Arrays.toString (данные)); } else if (начало ›= конец) { данные[индекс] = начало; помощник(данные, конец, начало - 1, индекс + 1); помощник(данные, конец, начало - 1, индекс); } } ``` Это основа алгоритма. Это производит все возможные комбинации в диапазоне от конца до начала. 08.04.2020
  • Вызовите это с помощью end = 0 и start = 2. Это означает, что результат будет следующим: [2, 1, 0] [2, 1] [2, 0] [1, 0] Начало определено как 2. Он помещает это число в данные [0], затем снова вызывает помощник, но с start-1 и index+1. Он перейдет к данным [1] и поместит число 2-1. Затем он идет еще дальше и помещает 0 в data[2]. Data[] заполнен, поэтому распечатайте его. Затем я вызываю ту же функцию, но с размером данных [] равным 2, а не 3. Он идет и помещает данные [0] = 2, данные [1] = 1. Data[] заполнен, распечатайте его. Теперь вернитесь к предыдущей функции, где мы поместили данные [1] = 1, а теперь поместили данные [1] = 0. Это объясняет, что я пытаюсь? 08.04.2020
  • Давайте продолжим обсуждение в чате. 08.04.2020
  • Не публикуйте неполные фрагменты кода в комментариях, опубликуйте полный работающий пример в своем вопросе. 09.04.2020
  • Я добавил, поможет? 09.04.2020
  • Рабочий код, опубликованный вами вывод никогда не создавался этим кодом. 09.04.2020
  • Да, ты прав, я пропустил ; после helper(data, 0, 2, 0). Теперь хорошо? 09.04.2020
  • А как насчет условия r > 1? Кроме того, теперь должно стать понятно, что вы создаете всего три массива, а зачем получать семь результатов. А теперь подумайте, как это должно работать, когда все эти шаги выполняются параллельно с использованием одного и того же массива? 09.04.2020
  • Но вы не используете один и тот же массив каждый раз. Для каждого r создается новый массив данных[r]. Поэтому я хочу запускать каждый r параллельно. 09.04.2020
  • Но в helper вы разветвляете шаги, которые являются рекурсивными в последовательной операции. Когда вы измените его на последовательную форму, вы можете без проблем создать задачу для каждого r. Конечно, тогда вы должны удалить вызовы join. Тогда ваш код должен работать правильно. Остается только проблема с производительностью, т.е. задания не имеют одинаковой нагрузки и для меньшего массива печать является самой затратной частью, но печать синхронизируется внутри (ведь вывод всегда в последовательности). 09.04.2020
  • Во-первых, спасибо, что уделили мне столько времени, чтобы помочь мне! Если я вас правильно понял, то я должен вызвать `ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();` перед циклом r и создать новый HelperCalculator для каждого r, а затем вызвать pool.execute(calculator); правильно? Но я не должен использовать вызовы .join() или isDone() 09.04.2020
  • Вы можете вызвать join() после цикла (что требует запоминания объектов задачи), но не внутри цикла, так как выполнение не будет параллельным, если вы ожидаете завершения сразу после разветвления. 09.04.2020

Ответы:


1

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

Но есть и другая возможность; вообще не используйте массивы. Вы можете хранить комбинации в значениях int, поскольку каждое значение int представляет собой комбинацию битов. Это не только экономит много памяти, но вы также можете легко перебирать все возможные комбинации, просто увеличивая значение, так как перебор всех int чисел также перебирает все возможные комбинации битов¹. Единственное, что нам нужно реализовать, — это сгенерировать правильную строку для конкретного значения int, интерпретируя биты как числа в соответствии с их положением.

Для первой попытки мы можем пойти по простому пути и использовать уже существующие классы:

public static void main(String[] args) {
    long t0 = System.nanoTime();
    combinations(10, 25);
    long t1 = System.nanoTime();
    System.out.println((t1 - t0)/1_000_000+" ms");
    System.out.flush();
}
static void combinations(int start, int end) {
    for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
        System.out.println(
            BitSet.valueOf(new long[]{i}).stream()
                  .mapToObj(b -> String.valueOf(b + start))
                  .collect(Collectors.joining(", ", "[", "]"))
        );
    }
}

Метод использует эксклюзивный конец, поэтому для вашего примера вам нужно вызвать его как combinations(0, 3), и он напечатает

[0]
[1]
[0, 1]
[2]
[0, 2]
[1, 2]
[0, 1, 2]
3 ms

конечно, сроки могут отличаться

Для приведенного выше примера combinations(10, 25) он печатает все комбинации, за которыми следует 3477 ms на моей машине. Это звучит как возможность для оптимизации, но мы должны сначала подумать о том, какие операции требуют каких затрат.

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

Давайте определим долю стоимости, создав новый PrintStream, отключив автоочистку при разрывах строк и используя безумно большой буфер, способный хранить весь вывод:

public static void main(String[] args) {
    System.setOut(new PrintStream(
        new BufferedOutputStream(new FileOutputStream(FileDescriptor.out),1<<20),false));
    long t0 = System.nanoTime();
    combinations(10, 25);
    long t1 = System.nanoTime();
    System.out.flush();
    long t2 = System.nanoTime();
    System.out.println((t1 - t0)/1_000_000+" ms");
    System.out.println((t2 - t0)/1_000_000+" ms");
    System.out.flush();
}
static void combinations(int start, int end) {
    for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
        System.out.println(
            BitSet.valueOf(new long[]{i}).stream()
                  .mapToObj(b -> String.valueOf(b + start))
                  .collect(Collectors.joining(", ", "[", "]"))
        );
    }
}

На моей машине он печатает что-то в порядке

93 ms
3340 ms

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

static void combinations(int start, int end) {
    for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
        System.out.println(bits(i, start));
    }
}
static String bits(int bits, int offset) {
    StringBuilder sb = new StringBuilder().append('[');
    for(;;) {
        int bit = Integer.lowestOneBit(bits), num = Integer.numberOfTrailingZeros(bit);
        sb.append(num + offset);
        bits -= bit;
        if(bits == 0) break;
        sb.append(", ");
    }
    return sb.append(']').toString();
}

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


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

Последовательный код уже привел задачу к форме, которая сводится к итерации от начального значения к конечному значению. Теперь мы переписываем этот код в ForkJoinTask (или подходящий подкласс), который представляет собой итерацию с начальным и конечным значением. Затем мы добавляем возможность разделить эту операцию на две, разделив диапазон посередине, чтобы получить две задачи, повторяющиеся в каждой половине диапазона. Это можно повторять до тех пор, пока мы не решим, что у нас достаточно потенциально параллельных заданий, и выполняем текущую итерацию локально. После локальной обработки мы должны дождаться завершения любой задачи, которую мы выделили, чтобы гарантировать, что завершение корневой задачи подразумевает завершение всех подзадач.

public class Combinations extends RecursiveAction {
    public static void main(String[] args) {
        System.setOut(new PrintStream(new BufferedOutputStream(
            new FileOutputStream(FileDescriptor.out),1<<20),false));
        ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
        long t0 = System.nanoTime();
        Combinations job = Combinations.get(10, 25);
        pool.execute(job);
        job.join();
        long t1 = System.nanoTime();
        System.out.flush();
        long t2 = System.nanoTime();
        System.out.println((t1 - t0)/1_000_000+" ms");
        System.out.println((t2 - t0)/1_000_000+" ms");
        System.out.flush();
    }

    public static Combinations get(int min, int max) {
        return new Combinations(min, 1, (1 << (max - min)) - 1);
    }

    final int offset, from;
    int to;

    private Combinations(int offset, int from, int to) {
        this.offset = offset;
        this.from = from;
        this.to = to;
    }

    @Override
    protected void compute() {
        ArrayDeque<Combinations> spawned = new ArrayDeque<>();
        while(getSurplusQueuedTaskCount() < 2) {
            int middle = (from + to) >>> 1;
            if(middle == from) break;
            Combinations forked = new Combinations(offset, middle, to);
            forked.fork();
            spawned.addLast(forked);
            to = middle - 1;
        }
        performLocal();
        for(;;) {
            Combinations forked = spawned.pollLast();
            if(forked == null) break;
            if(forked.tryUnfork()) forked.performLocal(); else forked.join();
        }
    }

    private void performLocal() {
        for(int i = from, stop = to; i <= stop; i++) {
            System.out.println(bits(i, offset));
        }
    }

    static String bits(int bits, int offset) {
        StringBuilder sb = new StringBuilder().append('[');
        for(;;) {
            int bit=Integer.lowestOneBit(bits), num=Integer.numberOfTrailingZeros(bit);
            sb.append(num + offset);
            bits -= bit;
            if(bits == 0) break;
            sb.append(", ");
        }
        return sb.append(']').toString();
    }
}

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

Есть два способа разделения. Примеры часто создают две или более разветвленных подзадач с последующим их объединением. Это может привести к большому количеству задач, которые просто ждут других. Альтернативой является разветвление подзадачи и изменение текущей задачи для представления другой. Здесь разветвленная задача представляет диапазон [middle, to], тогда как текущая задача изменена для представления диапазона [from, middle].

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

Это работает плавно, но, к сожалению, как и ожидалось, не ускоряет работу, так как самая дорогая часть — это печать.


¹ Использование int для представления всех комбинаций уменьшает поддерживаемую длину диапазона до 31, но имейте в виду, что такая длина диапазона подразумевает 2³¹ - 1 комбинаций, что довольно много для повторения. Если это все еще кажется ограничением, вы можете изменить код, чтобы использовать вместо него long. Поддерживаемой тогда длины диапазона 63, другими словами 2⁶³ - 1 комбинаций, достаточно, чтобы компьютер был занят до скончания века.

09.04.2020
  • Ух ты! Большое спасибо за помощь! Я точно многому научился! Спасибо, что уделили так много времени, чтобы помочь мне, когда я тоже был неясен! 09.04.2020
  • Просто чтобы я правильно вас понял, поскольку int может иметь только определенный размер (2 ^ 31 -1), существует ограничение на диапазон комбинаций. Максимально возможное значение равно combinations(1,32). Если бы я хотел перейти к combinations(1,64), мне пришлось бы изменить for(int i = 1[..] на for(long i = 1 [..] 09.04.2020
  • Да, но обратите внимание на цифры. Ограничение длины диапазона в 31 означает, что максимальное количество комбинаций составляет 2^31-1 == 2147483647. Если нам нужна одна миллисекунда для обработки (т.е. печати) одной комбинации, то обработка всех их занимает около 24 дней. Использование long допускает длину диапазона до 63, что позволяет 2^63-1 == 9223372036854775807 комбинаций. Даже если мы предположим, что скорость обработки каждой комбинации составляет всего одну наносекунду, нам потребуется почти триста лет, чтобы перебрать их все... 13.04.2020
  • Новые материалы

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

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..