Неожиданный многопоточный результат - PullRequest
8 голосов
/ 06 февраля 2009

Я написал пару классов Java & mdash; SingleThreadedCompute и MultithreadedCompute & mdash; чтобы продемонстрировать тот факт (или то, что я всегда считал фактом!), Что если вы распараллелите задачу, ориентированную на вычисления (без ввода-вывода) на одноядерной машине вы не получите ускорение. На самом деле я понимаю, что распараллеливание таких задач на самом деле замедляет ход событий, потому что теперь вам приходится иметь дело с переключением контекста. Ну, я запустил классы, и параллельная версия неожиданно работает быстрее: однопоточная версия постоянно работает на моей машине всего за 7 секунд, а многопоточная версия постоянно работает на моей машине чуть более 6 секунд. Кто-нибудь может объяснить, как это возможно?

Вот классы, если кто-то хочет посмотреть или попробовать сами.

public final class SingleThreadedCompute {
    private static final long _1B = 1000000000L; // one billion

    public static void main(String[] args) {
        long startMs = System.currentTimeMillis();

        long total = 0;
        for (long i = 0; i < _1B; i++) { total += i; }
        System.out.println("total=" + total);

        long elapsedMs = System.currentTimeMillis() - startMs;
        System.out.println("Elapsed time: " + elapsedMs + " ms");
    }
}

Вот многопоточная версия:

public final class MultithreadedCompute {
    private static final long _1B = 1000000000L; // one billion
    private static final long _100M = _1B / 10L;

    public static void main(String[] args) {
        long startMs = System.currentTimeMillis();

        System.out.println("Creating workers");
        Worker[] workers = new Worker[10];
        for (int i = 0; i < 10; i++) {
            workers[i] = new Worker(i * _100M, (i+1) * _100M);
        }

        System.out.println("Starting workers");
        for (int i = 0; i < 10; i++) { workers[i].start(); }

        for (int i = 0; i < 10; i++) {
            try {
                workers[i].join();
                System.out.println("Joined with thread " + i);
            } catch (InterruptedException e) { /* can't happen */ }
        }

        System.out.println("Summing worker totals");
        long total = 0;
        for (int i = 0; i < 10; i++) { total += workers[i].getTotal(); }
        System.out.println("total=" + total);

        long elapsedMs = System.currentTimeMillis() - startMs;
        System.out.println("Elapsed time: " + elapsedMs + " ms");
    }

    private static class Worker extends Thread {
        private long start, end;
        private long total;

        public Worker(long start, long end) {
            this.start = start;
            this.end = end;
        }

        public void run() {
            System.out.println("Computing sum " + start + " + ... + (" + end + " - 1)");
            for (long i = start; i < end; i++) { total += i; }
        }

        public long getTotal() { return total; }
    }
}

Вот результат работы однопоточной версии:

total=499999999500000000
Elapsed time: 7031 ms

А вот результат работы многопоточной версии:

Creating workers
Starting workers
Computing sum 0 + ... + (100000000 - 1)
Computing sum 100000000 + ... + (200000000 - 1)
Computing sum 200000000 + ... + (300000000 - 1)
Computing sum 300000000 + ... + (400000000 - 1)
Computing sum 400000000 + ... + (500000000 - 1)
Computing sum 500000000 + ... + (600000000 - 1)
Computing sum 600000000 + ... + (700000000 - 1)
Computing sum 700000000 + ... + (800000000 - 1)
Computing sum 800000000 + ... + (900000000 - 1)
Computing sum 900000000 + ... + (1000000000 - 1)
Joined with thread 0
Joined with thread 1
Joined with thread 2
Joined with thread 3
Joined with thread 4
Joined with thread 5
Joined with thread 6
Joined with thread 7
Joined with thread 8
Joined with thread 9
Summing worker totals
total=499999999500000000
Elapsed time: 6172 ms

РЕДАКТИРОВАТЬ: Информация об окружающей среде:

  • Microsoft Windows XP Professional Версия 2002, SP3
  • Dell Precision 670
  • Процессор Intel Xeon 2,80 ГГц, 1 МБ кэш-памяти второго уровня

Не уверен, как доказать, что это одноядерный компьютер, кроме как с указанием спецификации выше и отмечая, что когда я покупал машину (август 2005 г.), одноядерные были стандартом, и я не обновлялся до многоядерных (если это был даже вариант ... я не помню). Если есть что-то в Windows, я могу проверить не «Свойства системы» (где показана информация выше), дайте мне знать, и я проверю.


Вот пять последовательных серий ST и MT:

ПЯТЬ ОДНОСТОРОННИХ РАУНОВ:

общая = 499999999500000000 Истекшее время: 7000 мс

общая = 499999999500000000 Истекшее время: 7031 мс

общая = 499999999500000000 Истекшее время: 6922 мс

общая = 499999999500000000 Истекшее время: 6968 мс

общая = 499999999500000000 Истекшее время: 6938 мс


ПЯТЬ МНОГООБРАЗНЫХ РАБОТ: ​​

общая = 499999999500000000 Истекшее время: 6047 мс

общая = 499999999500000000 Истекшее время: 6141 мс

общая = 499999999500000000 Истекшее время: 6063 мс

общая = 499999999500000000 Истекшее время: 6282 мс

общая = 499999999500000000 Истекшее время: 6125 мс

Ответы [ 6 ]

6 голосов
/ 06 февраля 2009

Возможно, это связано с гиперпоточностью и / или конвейерной обработкой.

Из википедии по гиперпоточности :

Гиперпоточность - это шаг вперед по сравнению с суперпоточностью. Гиперпоточность (официально называемая Hyper-Threading Technology или HTT) - это запатентованная Intel технология, используемая для улучшения распараллеливания вычислений (выполнения нескольких задач одновременно), выполняемых на микропроцессорах ПК. Процессор с включенной гиперпоточностью рассматривается операционной системой как два процессора вместо одного. Это означает, что физически присутствует только один процессор, но операционная система видит два виртуальных процессора и распределяет рабочую нагрузку между ними.

Из википедии по трубопроводу :

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

3 голосов
/ 06 февраля 2009

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

В любом случае отключение JIT сработало (это значит, что оно привело фактические результаты в соответствие с ожидаемыми результатами). Мне пришлось отступить от одного миллиарда, поскольку это длилось вечно, поэтому я выбрал 100 миллионов вместо этого. Результаты намного больше соответствуют ожиданиям. Вот они:

ПЯТЬ БЕЗДЖИТНЫХ ОДНОПРОВОДНЫХ РАБОТ

общая = 4999999950000000 Истекшее время: 17094 мс

общая = 4999999950000000 Истекшее время: 17109 мс

общая = 4999999950000000 Истекшее время: 17219 мс

общая = 4999999950000000 Истекшее время: 17375 мс

общая = 4999999950000000 Истекшее время: 17125 мс


ПЯТЬ МНОГОФУНКЦИОНАЛЬНЫХ РАБОТ БЕЗ ДЖИТА

общая = 4999999950000000 Истекшее время: 18719 мс

общая = 4999999950000000 Истекшее время: 18750 мс

общая = 4999999950000000 Истекшее время: 18610 мс

общая = 4999999950000000 Истекшее время: 18890 мс

общая = 4999999950000000 Истекшее время: 18719 мс


Спасибо, ребята, за идеи и помощь.

3 голосов
/ 06 февраля 2009

На что похожа ваша остальная среда? Это повторяется?

По крайней мере в UNIX-системах такой длительный процесс, как этот, вероятно, будет иметь приоритетное значение; если у вас есть 10 потоков, каждый из них получает свой собственный фрагмент ЦП, и поэтому не будет накапливать столько же времени ЦП. Тогда он не потеряет приоритет над миф-индификацией. В целом, он получает больший общий объем ЦП.

Добавлена ​​

Просто для полноты, это то, что ваш код дает на двухъядерном Mac mini под OS / X 10.5.6

527 $ java MultithreadedCompute
Creating workers
Starting workers
Computing sum 100000000 + ... + (200000000 - 1)
Computing sum 0 + ... + (100000000 - 1)
Computing sum 400000000 + ... + (500000000 - 1)
Computing sum 200000000 + ... + (300000000 - 1)
Computing sum 500000000 + ... + (600000000 - 1)
Computing sum 600000000 + ... + (700000000 - 1)
Computing sum 700000000 + ... + (800000000 - 1)
Computing sum 800000000 + ... + (900000000 - 1)
Computing sum 900000000 + ... + (1000000000 - 1)
Computing sum 300000000 + ... + (400000000 - 1)
Joined with thread 0
Joined with thread 1
Joined with thread 2
Joined with thread 3
Joined with thread 4
Joined with thread 5
Joined with thread 6
Joined with thread 7
Joined with thread 8
Joined with thread 9
Summing worker totals
total=499999999500000000
Elapsed time: 3217 ms
528 $ java SingleThreadedCompute
total=499999999500000000
Elapsed time: 5651 ms
529 $ 

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

1 голос
/ 06 февраля 2009

Разница в десятую доли секунды? Шум от времени запуска (один) затопит это. Напишите что-нибудь, что длится минуту или две.

0 голосов
/ 06 февраля 2009

Просто потому, что это весело ... результат от машины с 8-ядерным сервером. AMD 2,7 ГГц, шанхайский процессор

Creating workers
Starting workers
Computing sum 0 + ... + (100000000 - 1)
Computing sum 100000000 + ... + (200000000 - 1)
Computing sum 300000000 + ... + (400000000 - 1)
Computing sum 500000000 + ... + (600000000 - 1)
Computing sum 600000000 + ... + (700000000 - 1)
Computing sum 200000000 + ... + (300000000 - 1)
Computing sum 800000000 + ... + (900000000 - 1)
Computing sum 700000000 + ... + (800000000 - 1)
Computing sum 900000000 + ... + (1000000000 - 1)
Computing sum 400000000 + ... + (500000000 - 1)
Joined with thread 0
Joined with thread 1
Joined with thread 2
Joined with thread 3
Joined with thread 4
Joined with thread 5
Joined with thread 6
Joined with thread 7
Joined with thread 8
Joined with thread 9
Summing worker totals
total=499999999500000000
Elapsed time: 444 ms
0 голосов
/ 06 февраля 2009

Попытка устранить разницу из-за HotSpot между кодом, выполняемым в однопотоковом и многопоточном вариантах:

public class ThreadedWorkers {
    private static final long _1B = 1000000000L; // one billion
    private static final long _100M = _1B / 10L;

    enum ThreadMode { SINGLE, SEQUENTIAL, MULTI };

    public static void main(String[] args) throws InterruptedException {
        final long startMs = System.currentTimeMillis();

        ThreadMode mode = args.length == 0 ? ThreadMode.SINGLE : ThreadMode.valueOf(args[0].toUpperCase());

        final long total = computeTotal( mode );

        System.out.println("total=" + total);

        long elapsedMs = System.currentTimeMillis() - startMs;

        System.out.println("Elapsed time: " + elapsedMs + " ms");
    }

    public static long computeTotal (ThreadMode mode) throws InterruptedException {
        Worker[] workers = new Worker[10];

        for (int i = 0; i < 10; i++)
            workers[i] = new Worker(i * _100M, (i+1) * _100M);

        switch (mode) {
            case SINGLE: {
                for (Worker worker : workers )
                    worker.run();

                break;
            } 

            case SEQUENTIAL:{
                for (Worker worker : workers ) {
                    worker.start();
                    worker.join();
                }

                break;
            }

            case MULTI: {
                for (Worker worker : workers )
                    worker.start();

                for (Worker worker : workers )
                    worker.join();

                break;
            }
        }

        System.out.println("Summing worker totals");

        long total = 0;

        for (Worker worker : workers )
            total += worker.getTotal();

        return total;
    }

    static class Worker extends Thread {
        private long start, end, total;

        public Worker(long start, long end) {
            this.start = start;
            this.end = end;
        }

        public void run() {
            System.out.println("Computing sum " + start + " + ... + (" + end + " - 1)");
            for (long i = start; i < end; i++) { total += i; }
        }

        public long getTotal() { return total; }
    }
}

Это все еще работает быстрее как мульти, чем в последовательном или одиночном режиме (примерно на 10 секунд на eee pc 900 - 23 против 13 секунд), даже если последовательное выполнение тех же методов, что и multi, такое же число раз.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...